在Slick中Upsert

Syn*_*sso 28 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
}
Run Code Online (Sandbox Code Playgroud)

ste*_*hke 36

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

注意

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

背景:

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

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

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

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

公平地说,隔离级别"可序列化"不会发生这种情况.但是这种隔离级别带来了巨大的性能损失,很少用于生产.此外,可序列化将需要您的应用程序的一些帮助:数据库管理系统通常不会真正序列化所有事务.但它会检测违反可序列化的重新发送的行为,并且只是中止有问题的交易.因此,您的应用程序必须准备好重新运行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)
  }
Run Code Online (Sandbox Code Playgroud)

  • 很好的答案和感谢基于光滑2.1.0的更新.我几乎错过了你的更新,因为它是基于光滑<2.1.0的详细答案的结尾.如果您在答案的顶部添加一些内容可能会有所帮助,最后将人们指向您的更新. (2认同)

Oli*_*ain 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))
}
Run Code Online (Sandbox Code Playgroud)

  • 我曾经也这么想过,这是一个非常常见的误解:-) 原子意味着,要么事务中的所有内容都被写入,要么什么都不写入。这并不意味着事务中的每个操作都会看到相同版本的数据。您的意思是“隔离”,这有几种“风格”,具体取决于您愿意做出的权衡。查看这篇维基百科文章 (http://en.wikipedia.org/wiki/Isolation_%28database_systems%29) 并阅读有关“幻读”和“隔离级别”的部分。 (8认同)
  • 这不会做你想象的那样。通常数据库不会在“可序列化”隔离级别上运行,而是在“已提交读”之类的隔离级别上运行。因此,虽然每个 SQL 语句都被很好地隔离,但同一事务中的两个语句可能会看到不同的数据。因此,为“user.firstOption”合成的“select”可能看不到任何匹配的数据库行。但并行事务可能会插入一个事务,甚至提交。当插入语句运行时,它将插入第二个匹配行。这可以通过两个单独的语句来实现。使用一个(!)upsert/merge 语句,这种情况就不会发生。 (7认同)