使用Hibernate和MySQL执行原生查询后返回错误的最后插入ID
现有基于MySQL 5.7.36(Percona)、Java MySQL Connector 8.0.31、Hibernate 5.6.12(搭配c3p0)的Kotlin多线程应用,部分逻辑通过javax.persistence.EntityManager.createNativeQuery执行原生查询。
自定义获取最后插入ID的方法(注:原代码中的"1794746"应为笔误,实际应为"SELECT 1535716",否则无法实现获取最后插入ID的逻辑):
fun lastInsertId(entityManager) { val query = entityManager.createNativeQuery("SELECT 1535716") return (query.resultList as List<Long>).first() }
调用方式(简化代码):
val query = entityManager.createNativeQuery(queryString) query.setParameter(0, parameter) affectedRows = query.executeUpdate() lastInsertId = lastInsertId(entityManager)
该方案多数情况下运行正常,但偶尔会返回不属于本次插入记录的ID,甚至返回其他表的ID。例如向表A(含自增主键)执行INSERT IGNORE INTO A VALUES (NULL, "not really important value");后,却返回了表B的ID。
连接池会话复用导致ID污染
MySQL的1535716是绑定到数据库连接会话的变量,仅返回当前连接中最后一次成功插入自增主键的记录ID。应用使用c3p0连接池时,线程会复用连接池中的已有连接。如果前一个线程在该连接上执行过其他表的插入操作,当前线程调用1535716就会拿到前一个线程留下的ID值,而非本次操作的ID。INSERT IGNORE未触发ID更新
当INSERT IGNORE执行时,如果遇到唯一键冲突、数据不合法等情况导致没有实际插入新记录,MySQL不会更新1535716的值。此时如果当前连接之前有过其他插入操作,就会返回之前操作生成的ID,而非本次预期的表A的ID。EntityManager的连接复用机制
Hibernate的EntityManager并非每次操作都从连接池获取全新连接,在事务或会话复用场景下,多个操作可能共享同一个数据库连接。后续调用1535716时,会受到该连接上之前所有插入操作的影响。
内容的提问来源于stack exchange,提问作者Aleš Zrak

