Подтвердить что ты не робот

Ускорение в Slick

Есть ли способ аккуратно выполнить операцию upsert в 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
}
4b9b3361

Ответ 1

Обновлен для поддержки поддержки upsert/merge в Slick 2.1

Внимание

Вам нужно использовать простой SQL-встраивание с помощью собственного MERGE в своей базе данных. Все испытания для имитации этого утверждения, скорее всего, приведут к неправильным результатам.

История:

Когда вы имитируете оператор upsert/merge, Slick должен будет использовать несколько операторов для достижения этой цели (например, кулак select, а затем либо вставку, либо инструкцию обновления). При запуске нескольких операторов в транзакции SQL они обычно не имеют того же уровня изоляции, что и один оператор. При различных уровнях изоляции вы будете испытывать странные эффекты в больших параллельных ситуациях. Таким образом, все будет хорошо работать во время тестов и не будет иметь странных эффектов в производстве.

Обычно база данных имеет более высокий уровень изоляции при запуске одного оператора как между двумя операторами в одной транзакции. Хотя в одном запущенном выражении не будут влиять другие операторы, которые работают параллельно. База данных либо заблокирует все, что коснется оператор, либо обнаруживает взаимосвязь между запущенными операторами и автоматически перезапускает проблемные утверждения, когда это необходимо. Этот уровень защиты не выполняется, когда выполняется следующий оператор в той же транзакции.

Таким образом может произойти (и будет!) следующее сценарий:

  • В первой транзакции оператор select за user.firstOption не находит строку базы данных для текущего пользователя.
  • Параллельная вторая транзакция вставляет строку для этого пользователя
  • Первая транзакция вставляет вторую строку для этого пользователя (аналогично phantom читать)
  • Вы либо закончите с двумя строками для одного и того же пользователя, либо первая транзакция завершится с нарушением ограничения, хотя его проверка была действительной (когда она выполнялась)

Чтобы быть справедливым, этого не произойдет с уровнем изоляции "serializable" . Но этот уровень изоляции приходит с огромным успехом, который редко используется в производстве. Кроме того, сериализуемое будет нуждаться в некоторой помощи от вашего приложения: система управления базами данных обычно не будет сериализовать всю транзакцию. Но он обнаружит нарушения, связанные с сериализуемым требованием, и просто прекратит транзакции в беде. Поэтому ваша заявка должна быть подготовлена ​​для повторной транзакции, которая прерывается (случайно) с помощью СУБД.

Если вы полагаетесь на нарушение ограничения, создайте приложение таким образом, чтобы он автоматически перезапускал транзакцию, не беспокоя пользователя. Это похоже на требование на уровне изоляции "сериализуемое".

Заключение

Используйте простой SQL для этого сценария или подготовьтесь к неприятным сюрпризам в производстве. Подумайте дважды о возможных проблемах с concurrency.

Обновление 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)
  }

Ответ 2

Очевидно, что это не (пока?) в 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))
}