众所周知,Model.find_or_create_by(X)
实际上是这样的:
- 按 X 选择
- 如果没有找到 -> 由 X 创建
- 返回记录(找到或创建)
并且步骤 1 和 2 之间可能存在竞争条件。为避免 X 在数据库中重复,应在 X 的字段集上使用唯一索引。但如果应用唯一索引,则其中一个竞争事务将失败异常(尝试创建 X 的副本时)。
如何实现#find_or_create_by
永远不会引发任何异常并始终按预期工作的“安全版本”?
众所周知,Model.find_or_create_by(X)
实际上是这样的:
并且步骤 1 和 2 之间可能存在竞争条件。为避免 X 在数据库中重复,应在 X 的字段集上使用唯一索引。但如果应用唯一索引,则其中一个竞争事务将失败异常(尝试创建 X 的副本时)。
如何实现#find_or_create_by
永远不会引发任何异常并始终按预期工作的“安全版本”?
答案在文档中
这是否是一个问题取决于应用程序的逻辑,但在行具有 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)
这是“SELECT-or-INSERT”的反复出现的问题,与流行的问题密切相关UPSERT
。即将到来的Postgres 9.5 提供了新INSERT .. ON CONFLICT DO NOTHING | UPDATE
的为每个提供干净的解决方案。
现在,我建议使用两个服务器端 plpgsql 函数来实现这种防弹实现。只有帮助函数INSERT
实现了更昂贵的错误捕获,并且只有在SELECT
不成功时才会调用。
这永远不会因为唯一的违规而引发异常,并且总是返回一行。
假设:
假设一个表以tbl
一列x
数据类型命名text
。相应地适应您的情况。
x
被定义UNIQUE
或PRIMARY 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 的通用函数,以适用于任何给定的列和表(但这超出了这个问题的范围):
UPSERT
Craig Ringer 在此相关答案中的基础知识:
find_or_create_by
在rails中有一个方法叫
但就我个人而言,我更喜欢先找到,如果没有找到,然后创建,(我认为它有更多的控制权)
Ex:
user = User.find(params[:id])
#User.create(#attributes) unless user
高温高压