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 のネイティブ アップサート/マージ サポート用に更新

注意

データベースのネイティブMERGEステートメントでプレーン SQL 埋め込みを使用する必要があります。このステートメントをシミュレートするためのすべての試行は、誤った結果につながる可能性が非常に高くなります。

バックグラウンド:

upsert / merge ステートメントをシミュレートする場合、Slick はその目標を達成するために複数のステートメントを使用する必要があります (たとえば、最初に select を実行し、次に insert または update ステートメントを実行します)。SQL トランザクションで複数のステートメントを実行する場合、通常、単一のステートメントと同じ分離レベルはありません。分離レベルが異なると、大量の同時発生状況で奇妙な影響が発生します。したがって、テスト中はすべて正常に動作し、本番環境では異常な結果で失敗します。

通常、データベースは、同じトランザクション内の 2 つのステートメント間として 1 つのステートメントを実行している間、より強力な分離レベルを持ちます。実行中の 1 つのステートメントは、並行して実行される他のステートメントの影響を受けません。データベースは、ステートメントが触れるすべてをロックするか、実行中のステートメント間の干渉を検出し、必要に応じて問題のあるステートメントを自動的に再開します。同じトランザクション内の次のステートメントが実行されると、このレベルの保護は維持されません。

したがって、次のシナリオが発生する可能性があります (そして発生します!):

  1. 最初のトランザクションでは、後ろの select ステートメントuser.firstOptionが現在のユーザーのデータベース行を見つけられません。
  2. 並行する 2 番目のトランザクションが、そのユーザーの行を挿入します。
  3. 最初のトランザクションは、そのユーザーの 2 番目の行を挿入します (ファントム読み取りに似ています) 。
  4. 同じユーザーに対して 2 つの行で終了するか、最初のトランザクションが制約違反で失敗しますが、そのチェックは (実行時に) 有効でした。

公平を期すために、これは分離レベル"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 に答える