web-dev-qa-db-ja.com

Slickのアップサート

Slickでアップサート操作をきちんと行う方法はありますか?以下は機能しますが、あいまい/冗長すぎるため、更新する必要のあるフィールドを明示的に指定する必要があります。

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
}
28
Synesso

Slick2.1のネイティブアップサート/マージサポート用に更新されました

注意

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

バックグラウンド:

アップサート/マージステートメントをシミュレートする場合、Slickはその目標を達成するために複数のステートメントを使用する必要があります(たとえば、選択を最初に実行してから、挿入または更新ステートメントを実行します)。 SQLトランザクションで複数のステートメントを実行する場合、それらは通常、単一のステートメントと同じ分離レベルを持ちません。分離レベルが異なると、大規模な同時状況で奇妙な影響が発生します。そのため、テスト中はすべて正常に動作し、本番環境では奇妙な影響で失敗します。

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

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

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

公平を期すために、これは分離レベルでは発生しません "serializable" 。ただし、この分離レベルにはhugeが付属しており、本番環境でパフォーマンスヒットが使用されることはめったにありません。さらに、シリアル化可能には、アプリケーションからの支援が必要になります。データベース管理システムは通常、すべてのトランザクションを実際にシリアル化できるわけではありません。ただし、シリアル化可能な要求に対する違反を検出し、問題のあるトランザクションを中止します。したがって、アプリケーションは、DBMSによって(ランダムに)中止されるトランザクションを再実行する準備をする必要があります。

制約違反の発生に依存している場合は、ユーザーに迷惑をかけずに問題のトランザクションを自動的に再実行するようにアプリケーションを設計してください。これは、分離レベル「シリアライズ可能」の要件に似ています。

結論

このシナリオではプレーンSQLを使用するか、本番環境での不快な驚きに備えてください。同時実行で発生する可能性のある問題についてよく考えてください。

アップデート5.8.2014:Slick2.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)
  }
35

どうやら これは(まだ?)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))
}
1