First_or_create yet ERROR:重复键值违反唯一约束

时间:2014-08-18 21:29:28

标签: ruby-on-rails-3 postgresql activerecord race-condition select-insert

我有以下代码:

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块,我怎么能解决这个问题?

2 个答案:

答案 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

如果这是实际的实现(?),那么对于竞争条件似乎完全打开。在第一笔交易SELECTSELECT之间,另一笔交易可以轻松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
)