Rails 创建或更新魔术?

我有一个名为 CachedObject的类,它存储由键索引的通用序列化对象。我希望这个类实现一个 create_or_update方法。如果找到一个对象,它将更新它,否则它将创建一个新的。

在 Rails 中有这样做的方法吗? 还是我必须编写自己的方法?

130050 次浏览

铁路6

Rails 6添加了一个提供此功能的 upsertupsert_all方法。

Model.upsert(column_name: value)

[ upsert ]它不实例化任何模型,也不触发 Active Record 回调或验证。

轨道5,4,3

如果您正在寻找“ upsert”(数据库在同一操作中执行更新或插入语句)类型的语句,则不需要。开箱即用,Rails 和 ActiveRecord 没有这样的特性。但是,您可以使用 翻转 gem。

Otherwise, you can use: find_or_initialize_by or find_or_create_by, which offer similar functionality, albeit at the cost of an additional database hit, which, in most cases, is hardly an issue at all. So unless you have serious performance concerns, I would not use the gem.

例如,如果没有找到名称为“ Roger”的用户,则实例化一个新的用户实例,其 name设置为“ Roger”。

user = User.where(name: "Roger").first_or_initialize
user.email = "email@example.com"
user.save

或者,您可以使用 find_or_initialize_by

user = User.find_or_initialize_by(name: "Roger")

在 Rails 3。

user = User.find_or_initialize_by_name("Roger")
user.email = "email@example.com"
user.save

您可以使用块,但是 该块只在记录是新的时候运行

User.where(name: "Roger").first_or_initialize do |user|
# this won't run if a user with name "Roger" is found
user.save
end


User.find_or_initialize_by(name: "Roger") do |user|
# this also won't run if a user with name "Roger" is found
user.save
end

如果不管记录的持久性如何,都要使用块,那么在结果上使用 tap:

User.where(name: "Roger").first_or_initialize.tap do |user|
user.email = "email@example.com"
user.save
end

你可以这样说:

CachedObject.where(key: "the given key").first_or_create! do |cached|
cached.attribute1 = 'attribute value'
cached.attribute2 = 'attribute value'
end

把这个添加到你的模型中:

def self.update_or_create_by(args, attributes)
obj = self.find_or_create_by(args)
obj.update(attributes)
return obj
end

有了它,你可以:

User.update_or_create_by({name: 'Joe'}, attributes)

续集 gem 添加了一个 update_or_create方法,它似乎可以完成您所寻找的工作。

在 Rails 4中,你可以添加一个特定的模型:

def self.update_or_create(attributes)
assign_or_new(attributes).save
end


def self.assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end

然后把它当成

User.where(email: "a@b.com").update_or_create(name: "Mr A Bbb")

Or if you'd prefer to add these methods to all models put in an initializer:

module ActiveRecordExtras
module Relation
extend ActiveSupport::Concern


module ClassMethods
def update_or_create(attributes)
assign_or_new(attributes).save
end


def update_or_create!(attributes)
assign_or_new(attributes).save!
end


def assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
end
end
end


ActiveRecord::Base.send :include, ActiveRecordExtras::Relation

这是个老问题了,但是为了完整,我把我的解决方案扔进了擂台。 我需要这个当我需要一个特定的发现,但不同的创建,如果它不存在。

def self.find_by_or_create_with(args, attributes) # READ CAREFULLY! args for finding, attributes for creating!
obj = self.find_or_initialize_by(args)
return obj if obj.persisted?
return obj if obj.update_attributes(attributes)
end

你一直在寻找的魔法已经添加到 Rails 6 现在您可以 翻转(更新或插入)。 对于单个记录的使用:

Model.upsert(column_name: value)

对于多个记录,使用 翻译:

Model.upsert_all(column_name: value, unique_by: :column_name)

注意 :

  • 这两种方法都不触发 ActiveRecord 回调或验证
  • Only _ by = > PostgreSQL 和 SQLite

通过链接 find_or_initialize_byupdate,这可以通过一种简单的方式来实现,从而避免(根据我的经验,通常) upsert的不必要的警告,并且最小化数据库调用。

例如:

Class.find_or_initialize_by(
key: "foo",
...
).update(
new_attribute: "bar",
...
)

将返回新创建或更新的对象。

值得注意的是,如果您的 find_or_initialize_by属性匹配多个 Class 实例,那么将只选择和更新“第一个”实例。