First_or_create все-таки ERROR: двойное значение ключа нарушает уникальное ограничение

У меня есть следующий код:

rating = user.recipe_ratings.where(:recipe_id => recipe.id).where(:delivery_id => delivery.id).first_or_create

Тем не менее, как-то мы получаем случайные PG::Error: ERROR: duplicate key value violates unique constraint ошибки от этого. Я не могу думать ни о какой причине, которая должна произойти, так как весь смысл first_or_create это предотвратить тех.

Это просто сумасшедшее состояние гонки? Как я могу решить это без сводящей с ума серии begin...rescue блоки?

1 ответ

Решение

Кажется, это происходит из-за типичного состояния гонки для случая "ВЫБРАТЬ или ВСТАВИТЬ".

Кажется, что Ruby в своей реализации предпочитает производительность над безопасностью. Цитирую "Ruby on Rails Guides":

first_or_create Метод проверяет, возвращается ли первый nil или нет. Если он вернется nil, затем create называется.

...

SQL, сгенерированный этим методом, выглядит следующим образом:

SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
BEGIN
INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at)
VALUES ('2011-08-30 05:22:57', 'Andy', 0, NULL, '2011-08-30 05:22:57')
COMMIT

Если это фактическая реализация (?), Она кажется полностью открытой для условий гонки. Другая транзакция может легко SELECT между первой транзакцией SELECT а также INSERT, А потом попробуй свой INSERT, что вызовет ошибку, о которой вы сообщили, поскольку первая транзакция вставила строку за это время.

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

Rails 6 добавляет новый метод create_or_find_by, который смягчает возможное состояние гонки с несколькими недостатками:

  • В базовой таблице должны быть соответствующие столбцы, определенные с уникальными ограничениями.
  • Нарушение уникального ограничения может быть вызвано только одним или, по крайней мере, меньшим числом заданных атрибутов. Это означает, что последующий find_by! может не найти подходящую запись, что затем вызоветActiveRecord::RecordNotFound исключение, а не запись с заданными атрибутами.
  • Пока мы избегаем состояния гонки между SELECT -> INSERT from find_or_create_by, на самом деле у нас есть другое условие гонки между INSERT -> SELECT, которое может быть инициировано, если DELETE между этими двумя операторами выполняется другим клиентом. Но для большинства приложений это условие значительно менее вероятно.
  • Он полагается на обработку исключений для обработки потока управления, который может быть немного медленнее.

def create_or_find_by(attributes, &block)
  transaction(requires_new: true) { create(attributes, &block) }
rescue ActiveRecord::RecordNotUnique
  find_by!(attributes)
end

Используя ваш пример:

rating = user.recipe_ratings.create_or_find_by(
  recipe_id: recipe.id,
  delivery_id: delivery.id
)
Другие вопросы по тегам