在Slick中Upsert

时间:2013-07-27 07:39:45

标签: scala slick

有没有办法可以在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
}

2 个答案:

答案 0 :(得分:35)

更新了Slick 2.1中的本机u​​psert / merge支持

注意

您必须将纯SQL嵌入与数据库本机MERGE语句一起使用。所有模拟此陈述的试验都很可能导致不正确的结果。

背景:

当您模拟upsert / merge语句时,Slick将必须使用多个语句来达到该目标(例如,先选择一个select,然后选择insert或update语句)。在SQL事务中运行多个语句时,它们通常不具有与单个语句相同的隔离级别。使用不同的隔离级别,您将在大量并发情况下遇到奇怪的效果。因此,在测试过程中一切都会正常工作,并且在生产过程中会出现奇怪的效果。

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

所以下面的场景可能(并且会!)发生:

  1. 在第一个事务中,user.firstOption后面的select语句找不到当前用户的数据库行。
  2. 并行第二个事务为该用户插入一行
  3. 第一个事务为该用户插入第二行(类似于phantom read
  4. 对于同一个用户,您要么以两行结束,要么第一个事务因违反约束而失败,尽管其检查有效(当它运行时)
  5. 公平地说,隔离级别"serializable"不会发生这种情况。但是这种隔离级别带有巨大的性能,在生产中很少使用。此外,可序列化将需要您的应用程序的一些帮助:数据库管理系统通常不会真正序列化所有事务。但它会检测违反可序列化的重新发送的行为,并且只是中止有问题的交易。因此,您的应用程序必须准备好重新运行由DBMS中止(随机)的事务。

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

    结论

    对此方案使用纯SQL或准备生产中的令人不快的意外。三思而后行可能的并发问题。

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

    使用Slick 2.1.0,现在可以对MERGE语句进行本机支持(请参阅release notes:“插入或更新支持,尽可能使用本机数据库功能”)。

    代码将如下所示(取自Slick test cases):

      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)
      }
    

答案 1 :(得分:1)

Apparently这不是(还没?)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))
}