我有以下代码:
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
块,我怎么能解决这个问题?
答案 0 :(得分:2)
这似乎源于" SELECT或INSERT"的典型竞争条件。情况下。
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可以大大减少竞争条件的时间范围。即使是安全的版本也不会花费更多。但我想他们有他们的理由 比较这个安全的实现:
答案 1 :(得分:1)
Rails 6添加了一个新的create_or_find_by方法,该方法减轻了可能的比赛条件,但有一些缺点:
- 基础表必须具有使用唯一约束定义的相关列。
- 唯一的约束冲突可能仅由一个或至少少于所有给定属性触发。这意味着随后的find_by!可能找不到匹配的记录,这将引发
ActiveRecord::RecordNotFound
异常,而不是具有给定属性的记录。- 虽然我们避免了
find_or_create_by
的SELECT-> INSERT之间的竞争条件,但实际上我们在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
)