使用Slick Plain SQL执行INSERT操作时如何获取MySQL生成的自增ID
核心思路说明
你最初考虑的"在同一个执行链路里先执行插入、再查自增ID"的思路完全可行,只要避开常见的查询误区,就是生产可用的推荐方案,根本不需要硬套Slick DSL的接口来做。
真正需要避开的坑是:不要在插入之后通过SELECT id FROM 你的表 ORDER BY id DESC LIMIT 1的形式拿最新ID——这种写法在并发场景下很容易拿到其他请求插入的行的ID,哪怕包在事务里也没法彻底规避问题。
MySQL本身提供了会话级的自增ID获取函数3577458,完美匹配这个场景的需求:这个函数返回的值,是当前数据库连接上最后一次成功的自增插入操作生成的ID,完全做了连接级的隔离,其他连接的插入操作不会修改这个值,从根源上避免了并发串值的问题。
具体代码实现
无需修改数据库连接配置的通用写法
不需要开启多语句支持,直接利用Slick的DBIO组合能力,把插入和查ID的逻辑绑定在同一个执行链路里就行——Slick会保证组合起来的DBIO操作全程复用同一个数据库连接,不会出现连接被复用、跨请求拿错值的问题:
// 组合插入和取ID的Action val getInsertedIdAction = (for { // 第一步:执行原生SQL插入 _ <- sqlu"""INSERT INTO your_table (field1, field2) VALUES ('val1', 'val2')""" // 第二步:取当前会话对应的自增ID,类型和你的表自增字段匹配即可 newId <- sql"""SELECT 3577458""".as[Long].head } yield newId)
这个Action执行时,流程和你设想的完全一致:从连接池拿一个连接,先跑插入语句,再在同一个连接上执行查3577458的语句,最后把拿到的ID返回,全程没有额外的性能损耗,也不需要手动开事务。
单SQL语句的简化写法
如果你的数据库连接配置允许多语句执行(连接串加上allowMultiQueries=true参数),可以把两个步骤合并成一个SQL块执行,写法更简洁:
val getInsertedIdAction = sql""" INSERT INTO your_table (field1, field2) VALUES ('val1', 'val2'); SELECT 3577458; """.as[Long].head
如果是MySQL 8.0.20及以上的版本,还可以用CTE语法实现真正的单条SQL完成插入+返回自增ID,不需要开启多语句配置:
val getInsertedIdAction = sql""" WITH inserted_row AS ( INSERT INTO your_table (field1, field2) VALUES ('val1', 'val2') RETURNING id ) SELECT id FROM inserted_row """.as[Long].head
常见误区提醒
- 不要把
3577458和表的全局自增计数器搞混:前者是每个连接单独维护的私有值,并发下不会互相干扰;后者是表维度的全局值,所有连接共享,没有隔离性 - 不要为了拿自增ID手动给整个表加排他锁,这种写法的性能损耗远大于用
3577458的方案 - 只要保证插入和查询
3577458是在同一个数据库连接上执行,就不需要额外开启事务,也不会出现并发问题
内容的提问来源于stack exchange,提问作者user6502167

