slick-pg 为何在批量 upsert 搭配 RETURNING 时生成 SQL 失败?
结论
PostgreSQL 支持多行 INSERT ... ON CONFLICT ... DO UPDATE ... RETURNING。从题目中的执行结果看,当前 Slick、slick-pg 和 profile 配置下,insertOrUpdateAll 与 returning(query) 的组合受限。
由于缺少具体版本、profile 定义和调用栈,尚不能认定这是某个版本的已知 bug,也不能断言所有 slick-pg 版本都不支持。如果需要立即实现,可以用 Slick Plain SQL 显式编写 upsert 并读取返回结果。PostgreSQL INSERT 文档
为什么另外两种写法可以工作?
普通批量插入、批量 upsert 和带返回结果的批量 upsert,涉及不同的 SQL 构造和结果处理路径。前两种操作成功,不代表第三种组合也已实现。
在 Slick 3.6.1 的源码中,InsertBuilderResult.buildMultiRowInsert 的默认实现会直接抛出该异常,具体构造器需要覆盖它才能生成多行语句。结合题目中的现象,可以判断失败路径没有提供所需的多行 SQL 构造能力。至于由哪个 profile 或构造器触发,还需要结合实际版本和调用栈确认。Slick SQL 构造器源码
异常发生在 SQL 生成阶段,调整数据库中的 DEFAULT 'foo' 无法解决这个构造器错误。
用 Plain SQL 实现批量 upsert 并返回记录
原示例只提交 id,需要先确定发生冲突时的业务行为。如果希望新记录使用默认 name,已有记录保留原来的 name,并返回两种情况下的记录,可以这样写:
INSERT INTO foo (id)
VALUES (5), (6)
ON CONFLICT (id) DO UPDATE
SET id = EXCLUDED.id
RETURNING id, name;
这条语句在冲突时将 id 更新为相同的值,同时保留 name。这是此处明确采用的冲突策略,尚未核实它与原 insertOrUpdateAll 生成的 SQL 完全一致。
在 Slick 中,沿用项目现有 profile 的 api 导入和数据库实例 db,可以这样读取结果:
val firstId = 5
val secondId = 6
val action = sql"""
INSERT INTO foo (id)
VALUES ($firstId), ($secondId)
ON CONFLICT (id) DO UPDATE
SET id = EXCLUDED.id
RETURNING id, name
""".as[(Int, Option[String])]
val result = db.run(action)
sql"""...""".as[...] 用于读取结果集,插值中的值会作为绑定参数传递。这里不要使用只返回受影响行数的 sqlu。Slick Plain SQL 文档
示例使用 Option[String],因为原始 DDL 中的 name 没有 NOT NULL 约束。默认值不能阻止其他操作显式写入 NULL。如果要映射回原来的 Foo(id: Int, name: String),需要先明确如何处理这种情况。
使用时需要注意什么?
- 将字段更新为相同值仍属于
UPDATE,可能触发 UPDATE trigger,并产生更新开销。 - 改成
ON CONFLICT DO NOTHING RETURNING ...会跳过冲突行,无法返回这些已有记录。PostgreSQL 的RETURNING只返回实际插入或更新的行。PostgreSQL RETURNING 说明 - 不要按返回位置匹配输入位置,应使用返回的
id建立对应关系。 - 如果希望继续使用查询 DSL,先记录 Slick、slick-pg、PostgreSQL JDBC driver 的版本及实际 profile,再用题目中的最小示例确认支持情况。方法能通过 Scala 编译,并不意味着对应 SQL 路径可用,也不能保证升级后一定修复。
备注:内容仅供参考。
