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
)