8

众所周知,Model.find_or_create_by(X)实际上是这样的:

  1. 按 X 选择
  2. 如果没有找到 -> 由 X 创建
  3. 返回记录(找到或创建)

并且步骤 1 和 2 之间可能存在竞争条件。为避免 X 在数据库中重复,应在 X 的字段集上使用唯一索引。但如果应用唯一索引,则其中一个竞争事务将失败异常(尝试创建 X 的副本时)。

如何实现#find_or_create_by永远不会引发任何异常并始终按预期工作的“安全版本”?

4

3 回答 3

8

答案在文档中

这是否是一个问题取决于应用程序的逻辑,但在行具有 UNIQUE 约束的特定情况下,可能会引发异常,只需重试:

begin
  CreditAccount.find_or_create_by(user_id: user.id)
rescue ActiveRecord::RecordNotUnique
  retry
end

解决方案 1

您可以在您的模型中实现以下内容,或者如果您需要保持 DRY,则可以在关注中实现以下内容

def self.find_or_create_by(*)
  super
rescue ActiveRecord::RecordNotUnique
  retry
end

用法:Model.find_or_create_by(X)


解决方案 2

或者,如果您不想覆盖find_or_create_by,您可以将以下内容添加到您的模型中

def self.safe_find_or_create_by(*args, &block)
  find_or_create_by *args, &block
rescue ActiveRecord::RecordNotUnique
  retry
end

用法:Model.safe_find_or_create_by(X)

于 2015-10-07T09:59:31.067 回答
3

这是“SELECT-or-INSERT”的反复出现的问题,与流行的问题密切相关UPSERT。即将到来的Postgres 9.5 提供了新INSERT .. ON CONFLICT DO NOTHING | UPDATE的为每个提供干净的解决方案。

Postgres 9.4 的实现

现在,我建议使用两个服务器端 plpgsql 函数来实现这种防弹实现。只有帮助函数INSERT实现了更昂贵的错误捕获,并且只有在SELECT不成功时才会调用。

永远不会因为唯一的违规而引发异常,并且总是返回一行。

假设:

  • 假设一个表以tbl一列x数据类型命名text。相应地适应您的情况。

  • x被定义UNIQUEPRIMARY KEY

  • 您想从基础表 ( return a record (found or created)) 返回整行。

  • 在许多情况下,该行已经存在。(不一定是大多数情况,SELECT比 . 便宜很多INSERT。)否则尝试第一个可能更有效INSERT

辅助功能:

CREATE OR REPLACE FUNCTION f_insert_x(_x text)
  RETURNS SETOF tbl AS
$func$
BEGIN
   RETURN QUERY
   INSERT INTO tbl(x) VALUES (_x) RETURNING *;

EXCEPTION WHEN UNIQUE_VIOLATION THEN  -- catch exception, no row is returned
   -- do nothing
END
$func$ LANGUAGE plpgsql;

主功能:

CREATE OR REPLACE FUNCTION f_x(_x text)
  RETURNS SETOF tbl AS
$func$
BEGIN
   LOOP
      RETURN QUERY
      SELECT * FROM tbl WHERE x = _x
      UNION  ALL
      SELECT * FROM f_insert_x(_x)  -- only executed if x not found
      LIMIT  1;

      EXIT WHEN FOUND;       -- else keep looping
   END LOOP;
END
$func$ LANGUAGE plpgsql;

称呼:

SELECT * FROM f_x('foo');

SQL Fiddle演示。

该功能基于我在此相关答案中制定的内容:

那里有详细的解释和链接。

我们还可以创建一个具有多态返回类型和动态 SQL 的通用函数,以适用于任何给定的列和表(但这超出了这个问题的范围):

UPSERTCraig Ringer 在此相关答案中的基础知识:

于 2015-10-07T03:44:27.653 回答
1

find_or_create_by在rails中有一个方法叫

链接将帮助您更好地理解它

但就我个人而言,我更喜欢先找到,如果没有找到,然后创建,(我认为它有更多的控制权)

Ex:

user = User.find(params[:id])
#User.create(#attributes) unless user

高温高压

于 2013-01-15T06:30:21.877 回答