我有以下代码:
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
块的情况下解决此问题?
这似乎源于“ SELECT或INSERT”情况的典型竞赛条件。
在实现中,Ruby似乎选择performance而不是safety”。引用"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
,这将引发您报告的错误,因为在此期间第一个事务已插入行。
race condition,但有一些缺点:
基础表必须具有使用唯一约束定义的相关列。
- 唯一约束冲突可能仅由一个或至少少于所有给定属性触发。这意味着随后的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 )