Upsert в 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
}

person Synesso    schedule 27.07.2013    source источник


Ответы (2)


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

Внимание

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

Задний план:

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

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

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

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

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

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

Заключение

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

Обновление 5.8.2014: 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)
  }
person stefan.schwetschke    schedule 24.09.2013
comment
Отличный ответ и спасибо за обновление на основе slick 2.1.0. Я почти пропустил ваше обновление, потому что оно находится в конце вашего подробного ответа, основанного на slick ‹ 2.1.0. Может быть полезно, если вы добавите что-то в начало ответа, указав людям на ваше обновление в конце. - person drstevens; 17.09.2014
comment
Есть ли удобный способ (Slick 3.0) сделать пакет insertOrUpdate? То есть без Action sequence of single insertOrUpdate? - person User; 30.07.2015
comment
@Ixx Пожалуйста, не захватывайте ветку. Лучше опубликовать отдельный (дополнительный) вопрос. Но, конечно, вы можете опубликовать ссылку на свой вопрос здесь. - person stefan.schwetschke; 31.07.2015
comment
Вопрос называется Upsert в Slick, и у него нет номера версии, почему он угоняет? - person User; 31.07.2015
comment
@Ixx Извините, возможно, я использовал неправильную формулировку. Никаких обвинений не планировалось. Это просто с прагматической точки зрения: вы, вероятно, получите лучшие ответы, если создадите новый вопрос. Просто люди обычно делают это здесь, и обычно это работает нормально. - person stefan.schwetschke; 01.08.2015
comment
Хорошо, нет проблем - я знаю, что вы не можете бесконечно редактировать этот ответ для всех возможных вариантов и будущих версий Slick ... Я открою новый вопрос, если мне это снова понадобится. - person User; 01.08.2015
comment
@Ixx это не проблема. Я бы с радостью обновил ответ. Просто я не знаю правильного ответа на вашу проблему. Вот почему я думаю, что Уилсон высшего уровня даст вам лучший результат, чем комментарий в той теме... - person stefan.schwetschke; 02.08.2015
comment
Примечание. InsertOrUpdate, по-видимому, работает только в MySQL. - person Jethro; 02.01.2020

Очевидно этого нет (пока?) в 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))
}
person OlivierBlanvillain    schedule 23.09.2013
comment
Это не будет делать то, что вы думаете. Обычно базы данных работают не на сериализуемом уровне изоляции, а в чем-то вроде фиксации чтения. Таким образом, хотя каждый оператор SQL хорошо изолирован, два оператора в одной и той же транзакции могут видеть разные данные. Таким образом, выбор, синтезированный для user.firstOption, может не видеть ни одной соответствующей строки базы данных. Но параллельная транзакция может вставить одну и даже зафиксировать. Когда оператор вставки запускается, он вставляет вторую соответствующую строку. Это возможно с помощью двух отдельных операторов. С одним (!) оператором upsert/merge такая ситуация не может возникнуть. - person stefan.schwetschke; 24.09.2013
comment
Я думал, что транзакция атомарна (см. en.wikipedia.org/wiki/Database_transaction, база данных транзакция по определению должна быть атомарной, последовательной, изолированной и устойчивой). Можете ли вы предоставить некоторые источники/примеры для иллюстрации? - person OlivierBlanvillain; 24.09.2013
comment
Когда-то я тоже так думал, это очень распространенное заблуждение :-) Атомарность означает, что либо все в транзакции пишется, либо ничего не пишется. Это не означает, что каждая операция в транзакции видит одну и ту же версию данных. Вы имеете в виду изоляцию, и она предоставляется в нескольких вариантах, в зависимости от того, на какие компромиссы вы готовы пойти. Посмотрите на эту статью в Википедии (en.wikipedia.org/wiki/Isolation_%28database_systems%29) и прочитайте части о фантомных чтениях и уровнях изоляции. - person stefan.schwetschke; 25.09.2013