1. 概述
在使用 JDBC 进行数据库操作时,经常会遇到需要插入数据后获取数据库自动生成的主键(如自增 ID)的场景。本文将带你用原生 JDBC 实现这一功能,不依赖任何 ORM 框架,✅ 掌握底层原理,避免踩坑。
2. 环境准备
为了方便演示,我们使用内存型 H2 数据库,无需额外安装,适合快速测试。
Maven 依赖
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
</dependency>
测试表结构
我们创建一个简单的 persons
表,包含自增主键 id
和姓名字段 name
:
public class JdbcInsertIdIntegrationTest {
private static Connection connection;
@BeforeClass
public static void setUp() throws Exception {
connection = DriverManager.getConnection("jdbc:h2:mem:generated-keys", "sa", "");
connection
.createStatement()
.execute("create table persons(id bigint auto_increment, name varchar(255))");
}
@AfterClass
public static void tearDown() throws SQLException {
connection
.createStatement()
.execute("drop table persons");
connection.close();
}
// 后续示例代码将在此补充
}
⚠️ 注意:auto_increment
是 H2 和 MySQL 的语法,其他数据库如 PostgreSQL 使用 serial
,Oracle 使用序列,但 JDBC 获取生成键的机制是通用的。
3. 使用 RETURN_GENERATED_KEYS 标志
最常用的方式是通过 Statement.RETURN_GENERATED_KEYS
标志告诉 JDBC 驱动:我需要返回生成的主键。
PreparedStatement 方式
String QUERY = "insert into persons (name) values (?)";
try (PreparedStatement statement = connection.prepareStatement(QUERY, Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, "Foo");
int affectedRows = statement.executeUpdate();
assertThat(affectedRows).isPositive();
// 获取生成的主键
try (ResultSet keys = statement.getGeneratedKeys()) {
assertThat(keys.next()).isTrue();
assertThat(keys.getLong(1)).isGreaterThanOrEqualTo(1);
}
}
✅ 关键点:
prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)
:启用主键返回。getGeneratedKeys()
返回一个ResultSet
,即使只生成一个键。- 必须先调用
next()
移动游标,否则无法读取数据。
Statement 方式(适用于静态 SQL)
如果你用的是普通 Statement
,也可以同样处理:
try (Statement statement = connection.createStatement()) {
String query = "insert into persons (name) values ('Foo')";
int affectedRows = statement.executeUpdate(query, Statement.RETURN_GENERATED_KEYS);
assertThat(affectedRows).isPositive();
try (ResultSet keys = statement.getGeneratedKeys()) {
assertThat(keys.next()).isTrue();
assertThat(keys.getLong(1)).isGreaterThanOrEqualTo(1);
}
}
⚠️ 注意:executeUpdate(sql, RETURN_GENERATED_KEYS)
是 Statement
的重载方法,PreparedStatement
不支持这种写法,必须在 prepareStatement
时指定。
4. 指定返回特定列
有时候你不仅关心主键,还想获取其他由数据库生成的字段(比如默认值、触发器生成的值)。JDBC 也支持指定返回哪些列。
按列名指定
String QUERY = "insert into persons (name) values (?)";
try (PreparedStatement statement = connection.prepareStatement(QUERY, new String[] { "id" })) {
statement.setString(1, "Foo");
int affectedRows = statement.executeUpdate();
assertThat(affectedRows).isPositive();
try (ResultSet keys = statement.getGeneratedKeys()) {
assertThat(keys.next()).isTrue();
assertThat(keys.getLong(1)).isGreaterThanOrEqualTo(1);
}
}
✅ 说明:
- 传入列名数组
new String[]{"id"}
,明确告诉驱动只返回id
列。 - 这种方式更精确,尤其在多列自动生成时有用。
Statement 的用法
同样适用于 Statement
:
try (Statement statement = connection.createStatement()) {
int affectedRows = statement.executeUpdate("insert into persons (name) values ('Foo')",
new String[] { "id" });
assertThat(affectedRows).isPositive();
try (ResultSet keys = statement.getGeneratedKeys()) {
assertThat(keys.next()).isTrue();
assertThat(keys.getLong(1)).isGreaterThanOrEqualTo(1);
}
}
💡 小贴士:虽然通常只返回主键,但某些场景下(如 TIMESTAMP
默认值、计算列)可以返回多个字段,ResultSet
中会按指定顺序排列。
5. 总结
- ✅ 使用
Statement.RETURN_GENERATED_KEYS
是最通用的方式。 - ✅ 可通过列名数组精确控制返回哪些生成的字段。
- ✅
PreparedStatement
和Statement
都支持,但 API 略有不同。 - ✅ 始终使用 try-with-resources 管理资源,避免连接泄漏。
所有示例代码已托管至 GitHub:https://github.com/tech-tutorial/jdbc-generated-keys-demo(模拟地址)
掌握这些技巧后,你在使用 MyBatis、JPA 等框架时,也能更好理解它们底层是如何处理 @GeneratedValue
或 useGeneratedKeys
的,遇到问题不再一脸懵。