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 是最通用的方式。
  • ✅ 可通过列名数组精确控制返回哪些生成的字段。
  • PreparedStatementStatement 都支持,但 API 略有不同。
  • ✅ 始终使用 try-with-resources 管理资源,避免连接泄漏。

所有示例代码已托管至 GitHub:https://github.com/tech-tutorial/jdbc-generated-keys-demo(模拟地址)

掌握这些技巧后,你在使用 MyBatis、JPA 等框架时,也能更好理解它们底层是如何处理 @GeneratedValueuseGeneratedKeys 的,遇到问题不再一脸懵。


原始标题:Returning the Generated Keys in JDBC