Rails是创建还是更新魔术?


93

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

在Rails中有没有办法做到这一点,还是我必须编写自己的方法?

Answers:


172

导轨6

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

Model.upsert(column_name: value)

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

导轨5、4和3

如果要查找“ upsert”(数据库在同一操作中执行更新或插入语句)类型的语句,则不会。开箱即用,Rails和ActiveRecord没有这种功能。但是,您可以使用upsert gem。

否则,您可以使用:find_or_initialize_byfind_or_create_by,它们提供类似的功能,尽管要付出额外的数据库命中代价,在大多数情况下,这根本就不是问题。因此,除非您有严重的性能问题,否则我不会使用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

您可以使用一个块,但是该块仅在记录为new时运行

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


1
该文档所指向的源代码表明,它不能像您所暗示的那样起作用-仅当相应的记录不存在时,该块才传递给新方法。Rails中似乎没有“ upsert”魔术,您必须将其分成两个Ruby语句,一个用于对象选择,一个用于属性更新。
sameers 2014年

@sameers我不确定我明白你的意思。您认为我的意思是什么?
Mohamad 2014年

1
哦,我明白您的意思了-两种形式都可以,find_or_initialize_by并且find_or_create_by接受一个障碍。我以为您的意思是,不管记录是否存在,为了进行更新,都会以记录对象作为参数向下传递一个块。
sameers 2014年

3
这有点误导人,不一定是答案,而是API。人们会期望该块无论如何都将通过,因此可以相应地创建/更新。相反,我们必须将其分解为单独的语句。嘘 <3 Rails :)
Volte

32

在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")

或者,如果您希望将这些方法添加到初始化程序中放入的所有模型中:

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

1
assign_or_new如果存在表的第一行,将不返回该行,然后更新该行吗?看来是为我做的。
史蒂夫·克莱恩

User.where(email: "a@b.com").first如果找不到,将返回nil。确保您有一个where范围
montrealmike 2015年

请注意,updated_at由于assign_attributes使用而不会被触及
-lshepstone,

不是因为您使用了assing_or_new,而是因为保存而使用了update_or_create
montrealmike

13

将此添加到您的模型:

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)

第二部分我不会工作。如果没有要更新的记录ID,则无法从类级别更新单个记录。
Aeramor

1
obj = self.find_or_create_by(args); obj.update(attributes); 返回obj;将起作用。
veeresh yh

12

您一直在寻找的魔术已添加到“ Rails 6 现在您可以升级(更新或插入)”中了。对于单记录使用:

Model.upsert(column_name: value)

对于多个记录,请使用upsert_all

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

注意事项

  • 两种方法都不会触发Active Record回调或验证
  • unique_by =>仅PostgreSQL和SQLite

1

老问题了,但是为了完整起见,把我的解决方案扔进了戒指。当我需要特定的查找但如果不存在则需要其他创建时,我就需要此。

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

0

您可以在以下一条语句中完成此操作:

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

9
这将不起作用,因为只有找到一个原始记录,它才会返回原始记录。OP要求一种解决方案,即使找到记录,该解决方案也总是更改该值。
JeanMertz 2014年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.