Rails find_or_create_by : 查找具有给定属性的第一条记录,或创建一条记录

  def find_or_create_by(attributes, &block)
    find_by(attributes) || create(attributes, &block)
  end

但请注意,此方法不是原子方法,不是线程安全的。它首先运行SELECT,如果没有结果,则尝试INSERT。如果还有其他线程或进程,则这两个调用之间存在竞争状态,并且最终可能会有两个相似的记录。

是否存在问题取决于应用程序的逻辑,但是在某些行具有UNIQUE约束的特定情况下,可能会引发异常,

即增加数据库字段唯一索引,通过以下代码重试:

begin
    CreditAccount.transaction(requires_new: true) do
        CreditAccount.find_or_create_by(user_id: user.id)
    end
rescue ActiveRecord::RecordNotUnique
    retry
end

在Rails 6中引入了 create_or_find_by, create_or_find_by! 方法,当然你也可以在你的Rails 5 + 中自己实现该逻辑

def create_or_find_by(attributes, &block)
    transaction(requires_new: true) { create(attributes, &block) }
rescue ActiveRecord::RecordNotUnique
    find_by!(attributes)
end

# Like #create_or_find_by, but calls
# {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception
# is raised if the created record is invalid.
def create_or_find_by!(attributes, &block)
    transaction(requires_new: true) { create!(attributes, &block) }
rescue ActiveRecord::RecordNotUnique
    find_by!(attributes)
end

尝试在具有唯一约束的表中创建给定属性的记录。如果数据库已经存在唯一约束的记录,则会引起插入异常,并捕获ActiveRecord::RecordNotUnique,并使用 #find_by 查找具有这些属性的记录。

这与 #find_or_create_by 相似,但是避免了过时读取的问题,因为find_or_create_by需要首先查询表,然后在找不到行的情况下尝试插入行。SELECT和INSERT语句之间留有时间间隔,这可能会在高吞吐量应用程序中引起问题。 不过,#create_or_find_by有几个缺点:
  • 表中必须具有唯一约束的字段。
  • 唯一约束冲突可能仅由一个或至少少于所有给定属性触发。这意味着后续的#find_by可能找不到匹配的记录,该记录将返回nil,而不是给定属性的记录。(其他字段唯一性约束触发异常, 数据库并不存在给定属性的记录,也意味着该方法不能滥用)
  • 它依靠异常处理来处理控制流,这可能会稍微慢一些。如果所有给定属性都被唯一约束覆盖,则此方法将始终返回一条记录,但是如果尝试创建并且由于 Rails的验证 validation 而失败,则该方法将不会持久保存,这将和#create返回的结果一样。(用异常类来处理逻辑, 违背了正常处理机制的初衷)
  • 主键加速用尽的问题,每一次rollback将消耗主键数量

如果主键类型是int,可能会出现主键用尽的问题(但是从Rails 5.0 开始就使用bigint,即使您每天调用此方法1000万次,用完ID也要花费25亿年。),一定程度上,您不需要担心此问题

0条评论 顺序楼层
请先登录再回复