必须在同连接、同事务、未提交前提下用数据库原生机制原子化获取插入ID:PostgreSQL用RETURNING,MySQL用LAST_INSERT_ID()或getGeneratedKeys(),SQL Server用OUTPUT或SCOPE_IDENTITY()。

直接说结论:不能用SELECT LAST_INSERT_ID()或@@IDENTITY跨语句查,也不能靠事务外二次查询;必须在同连接、同作用域、不提交事务的前提下,用数据库原生机制原子化获取。
PostgreSQL 必用 RETURNING,别写 RETURNING *
PostgreSQL 的 RETURNING 是唯一真正原子、无需额外 round-trip 的方式。它和 INSERT 一起执行,结果集随语句立即返回。
- 只返回必要字段,比如
INSERT INTO users (name) VALUES ('alice') RETURNING id,避免RETURNING *带来冗余数据和解析开销 - 批量插入时,
RETURNING返回多行,和VALUES行数严格对应,可直接映射 - 驱动必须支持多结果集(如
psycopg2要调cursor.fetchall(),不是cursor.fetchone()) - 别名很重要:如果写
RETURNING id AS user_id,应用层就得按user_id取值,否则字段名不匹配会报错
MySQL 只能靠 LAST_INSERT_ID(),但必须“立刻、同连接、单条”
MySQL 没有 RETURNING,LAST_INSERT_ID() 是唯一可靠选择——但它不是函数调用,而是会话级状态变量,行为高度依赖上下文。
- 必须在同一线程、同一 JDBC
Connection对象里,紧接executeUpdate()后调用,中间不能穿插其他 INSERT/REPLACE/INSERT ... SELECT - 批量插入(
INSERT INTO t VALUES (1),(2),(3))只返回第一个 ID,不是数组,别指望它返回全部 - 如果表没自增主键,
LAST_INSERT_ID()返回 0 且不报错,应用层得自己判断是否真生成了 ID - JDBC 层更推荐用
getGeneratedKeys()(传Statement.RETURN_GENERATED_KEYS),它底层就是封装了LAST_INSERT_ID(),但自动做了连接绑定和类型转换
SQL Server 别漏写 INSERTED. 前缀,SCOPE_IDENTITY() 才安全
OUTPUT 子句功能最强,但语法最易错;SCOPE_IDENTITY() 更轻量,适合简单场景。两者都要求作用域隔离。
-
OUTPUT INSERTED.id必须带INSERTED.,写成OUTPUT id直接报错Invalid column name 'id' -
SCOPE_IDENTITY()是首选,它只看当前作用域(比如当前批处理或存储过程),不会被触发器里的 INSERT 干扰;@@IDENTITY会返回触发器生成的 ID,极危险 -
IDENT_CURRENT('table')是全局取值,高并发下完全不可靠,生产环境禁止使用 - 如果插入失败(如违反唯一约束),
OUTPUT不返回任何行,@@ROWCOUNT为 0,得靠它判断是否真插入成功
JDBC 和 MyBatis 中的常见陷阱
ORM 封装掩盖了细节,但底层仍是数据库机制。很多问题源于配置缺失或误解。
- MyBatis 的
useGeneratedKeys="true"必须配对keyProperty="id"和keyColumn="id",缺一不可,否则id字段不会被赋值 - JDBC
PreparedStatement构造时没传Statement.RETURN_GENERATED_KEYS,后续调getGeneratedKeys()返回空结果集,不是 bug 是预期行为 - Spring 的
@Transactional方法里,如果用了连接池(如 HikariCP),必须确保getGeneratedKeys()在同一个物理连接上执行——默认是满足的,但若手动切换连接(如用DataSourceUtils.getConnection()再释放),就可能断开 - MyBatis-Plus 的
insert()能自动回填id,前提是实体类@TableId(type = IdType.AUTO)正确标注,且数据库字段确实是自增
最常被忽略的一点:所有这些机制都只在事务未提交时有效。一旦你为了“先拿 ID 再插入子表”而提前 commit(),就彻底失去原子性——父子数据不一致的风险不是理论问题,是线上真实故障来源。

















