1

我有以下代码:

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如果没有一系列令人抓狂的障碍,我该如何解决这个问题?

4

2 回答 2

3

这似乎源于“选择或插入”案例的典型竞争条件

Ruby在其实现中似乎选择了性能而不是安全性。引用“Ruby on Rails 指南”

first_or_create方法检查是否首先返回nil。如果它确实返回nil,则create调用 then。

...

此方法生成的 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 可以大大缩短竞争条件的时间范围。即使是安全版本也不会花费那么多。但我想他们有他们的理由。
比较这个安全的实现:

于 2014-08-18T22:21:18.207 回答
1

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
)
于 2020-01-22T17:54:48.380 回答