Slick での Upsert 質問する

Slick での Upsert 質問する

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
}

ベストアンサー1

Slick 2.1 のネイティブ upsert/merge サポートを更新しました

注意

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

背景:

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

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

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

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

公平に言えば、隔離レベルではこのようなことは起こりません「シリアル化可能」しかし、この分離レベルには巨大なパフォーマンス ヒットは、本番環境ではほとんど使用されません。さらに、シリアライズ可能では、アプリケーションからの支援が必要になります。データベース管理システムは、通常、すべてのトランザクションをシリアライズ可能とはしません。ただし、シリアライズ可能要件に対する違反を検出し、問題のあるトランザクションを中止します。そのため、アプリケーションは、DBMS によって (ランダムに) 中止されたトランザクションの再実行に備える必要があります。

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

結論

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

2014年5月8日更新: Slick 2.1.0ではネイティブMERGEサポートが追加されました

Slick 2.1.0では、MERGE文がネイティブサポートされるようになりました(リリースノート: 「可能な場合はネイティブ データベース機能を活用する挿入または更新のサポート」。

コードは次のようになります(巧妙なテストケース):

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

おすすめ記事