28

有没有办法可以在 Slick 中巧妙地进行 upsert 操作?以下工作但过于晦涩/冗长,我需要明确说明应更新的字段:

val id = 1
val now = new Timestamp(System.currentTimeMillis)
val q = for { u <- Users if u.id === id } yield u.lastSeen 
q.update(now) match {
  case 0 => Users.insert((id, now, now))
  case _ => Unit
}
4

2 回答 2

37

更新了 Slick 2.1 中的原生 upsert/merge 支持

注意力

您必须在数据库本机MERGE语句中使用纯 SQL 嵌入。所有模拟此陈述的试验很可能会导致错误的结果。

背景:

当您模拟 upsert / merge 语句时,Slick 将不得不使用多个语句来达到该目标(例如先选择一个选择,然后是一个插入或更新语句)。在 SQL 事务中运行多条语句时,它们的隔离级别通常与单个语句不同。使用不同的隔离级别,在海量并发情况下会遇到奇怪的效果。因此,在测试期间一切都会正常工作,但在生产中会出现奇怪的影响。

在同一事务中的两条语句之间运行一条语句时,数据库通常具有更强的隔离级别。而一个正在运行的语句不会受到其他并行运行的语句的影响。数据库将锁定语句涉及的所有内容,或者它将检测正在运行的语句之间的干扰并在必要时自动重新启动有问题的语句。当执行同一事务中的下一条语句时,这种保护级别不成立。

因此,可能(并且将会!)发生以下情况:

  1. 在第一个事务中,后面的 select 语句user.firstOption没有为当前用户找到数据库行。
  2. 并行的第二个事务为该用户插入一行
  3. 第一个事务为该用户插入第二行(类似于幻读
  4. 您要么以同一用户的两行结束,要么第一个事务因违反约束而失败,尽管它的检查是有效的(当它运行时)

公平地说,隔离级别"serializable"不会发生这种情况。但是这种隔离级别会带来巨大的性能损失,在生产中很少使用。此外,可序列化将需要您的应用程序提供一些帮助:数据库管理系统通常不会真正序列化所有事务。但它会检测对可序列化请求的违规行为,并中止有问题的事务。因此,您的应用程序必须准备好重新运行被 DBMS(随机)中止的事务。

如果您依赖约束违规的发生,请设计您的应用程序,使其自动重新运行相关事务而不会打扰用户。这类似于隔离级别“可序列化”的要求。

结论

在这种情况下使用普通 SQL 或为生产中的不愉快意外做好准备。三思而后行可能出现的并发问题。

5.8.2014 更新:Slick 2.1.0 现在具有原生 MERGE 支持

在 Slick 2.1.0 中,现在有对 MERGE 语句的本机支持(请参阅发行说明:“插入或更新支持,它在可能的情况下使用本机数据库功能”)。

代码将如下所示(取自Slick 测试用例):

  def testInsertOrUpdatePlain {
    class T(tag: Tag) extends Table[(Int, String)](tag, "t_merge") {
      def id = column[Int]("id", O.PrimaryKey)
      def name = column[String]("name")
      def * = (id, name)
      def ins = (id, name)
    }
    val ts = TableQuery[T]

    ts.ddl.create

    ts ++= Seq((1, "a"), (2, "b")) // Inserts (1,a) and (2,b)

    assertEquals(1, ts.insertOrUpdate((3, "c"))) // Inserts (3,c)
    assertEquals(1, ts.insertOrUpdate((1, "d"))) // Updates (1,a) to (1,d)

    assertEquals(Seq((1, "d"), (2, "b"), (3, "c")), ts.sortBy(_.id).run)
  }
于 2013-09-24T14:59:14.637 回答
1

显然,这不是(还?)在 Slick 中。

但是,您可能会尝试firstOption一些更惯用的东西:

val id = 1
val now = new Timestamp(System.currentTimeMillis)
val user = Users.filter(_.id is id)
user.firstOption match {
  case Some((_, created, _)) => user.update((id, created, now))
  case None => Users.insert((id, now, now))
}
于 2013-09-23T16:07:43.000 回答