You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Slick中批量插入或更新多行,无主键时按外键匹配执行upsert

Slick基于非主键联合字段实现Upsert(插入或更新)方案

前置准备

首先在数据库层为container、containerOrder(对应模型里的order字段)、vendor三个字段添加唯一联合索引,避免并发场景下插入重复数据,SQL参考(MySQL为例):

ALTER TABLE order_calculation 
ADD UNIQUE KEY uk_container_order_vendor (container, containerOrder, vendor);

方案1:逐行处理(适合小数据量,逻辑简单)

该方案逻辑易懂,无需适配不同数据库语法,适合单次处理数据量不超过百级的场景:

// 替换为你实际使用的数据库对应的Profile
import slick.jdbc.MySQLProfile.api._

def insertOrUpdateCalculations(calculations: Seq[OrderCalculationSummary]): Future[Seq[OrderCalculation]] = {
  val actions = calculations.map { calc =>
    // 构造三个唯一字段的匹配规则
    val matchQuery = orderCalculationTbl.filter { row =>
      row.container === calc.containerId &&
      row.order === calc.orderId &&
      row.vendor === calc.vendorId
    }

    // 先执行更新操作
    matchQuery.map(r => (r.volume, r.weight))
      .update((calc.volume, calc.weight))
      .flatMap { updateCnt =>
        if (updateCnt > 0) {
          // 更新成功直接返回匹配到的记录
          matchQuery.result.head
        } else {
          // 无匹配记录执行插入,返回带自增ID的新记录
          val newRecord = OrderCalculation(
            id = 0, // 自增ID占位,插入后会替换为真实值
            container = calc.containerId,
            order = calc.orderId,
            vendor = calc.vendorId,
            volume = calc.volume,
            weight = calc.weight
          )
          (orderCalculationTbl returning orderCalculationTbl.map(_.id)) += newRecord
            .map(generatedId => newRecord.copy(id = generatedId))
        }
      }
  }
  // 所有操作组合成事务执行,避免部分成功部分失败
  db.run(DBIO.sequence(actions).transactionally)
}

注意:该方案存在竞态条件,并发场景下如果两个请求同时查询到无匹配记录,同时插入会触发唯一索引冲突报错,对并发要求高的场景建议使用方案2。

方案2:数据库原生批量Upsert(适合大数据量、高并发场景)

利用数据库原生的Upsert语法实现原子操作,性能和安全性更高,以下分别给出主流数据库的实现参考:

MySQL版本(基于ON DUPLICATE KEY UPDATE)

import slick.jdbc.MySQLProfile.api._
import scala.concurrent.ExecutionContext.Implicits.global

def insertOrUpdateCalculations(calculations: Seq[OrderCalculationSummary]): Future[Seq[OrderCalculation]] = {
  if (calculations.isEmpty) return Future.successful(Nil)
  
  // 批量执行Upsert
  val upsertAction = sqlu"""
    INSERT INTO order_calculation (container, containerOrder, vendor, volume, weight)
    VALUES #${calculations.map(c => s"(${c.containerId}, ${c.orderId}, ${c.vendorId}, ${c.volume}, ${c.weight})").mkString(",")}
    ON DUPLICATE KEY UPDATE volume = VALUES(volume), weight = VALUES(weight)
  """

  // 执行完Upsert后查询所有匹配的记录返回
  val queryAction = orderCalculationTbl.filter { row =>
    calculations.map { calc =>
      row.container === calc.containerId &&
      row.order === calc.orderId &&
      row.vendor === calc.vendorId
    }.reduce(_ || _)
  }.result

  // 两个操作放在同一个事务中执行
  db.run((upsertAction >> queryAction).transactionally)
}

如果担心SQL拼接有注入风险,可替换为Slick预编译批量参数绑定写法。

PostgreSQL版本(基于ON CONFLICT DO UPDATE)

只需要替换上方代码中的upsertAction部分即可:

val upsertAction = sqlu"""
  INSERT INTO order_calculation (container, containerOrder, vendor, volume, weight)
  VALUES #${calculations.map(c => s"(${c.containerId}, ${c.orderId}, ${c.vendorId}, ${c.volume}, ${c.weight})").mkString(",")}
  ON CONFLICT (container, containerOrder, vendor) DO UPDATE 
  SET volume = EXCLUDED.volume, weight = EXCLUDED.weight
"""

内容的提问来源于stack exchange,提问作者zoran jeremic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 21:51:01