【问题标题】:Rails create or update magic?Rails 创造或更新魔法?
【发布时间】:2013-09-15 19:47:04
【问题描述】:

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

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

【问题讨论】:

    标签: ruby-on-rails activerecord


    【解决方案1】:

    导轨 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
    

    您可以使用块,但只有在记录是新记录时才会运行块

    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
    

    【讨论】:

    • 文档指向的源代码表明这并不像您暗示的那样工作 - 只有当相应的记录不存在时,才会将块传递给新方法。 Rails 中似乎没有“upsert”魔法——你必须将它分成两个 Ruby 语句,一个用于对象选择,一个用于属性更新。
    • @sameers 我不确定我理解你的意思。你认为我在暗示什么?
    • 哦...我现在明白你的意思了——find_or_initialize_byfind_or_create_by 两种形式都接受一个块。我以为您的意思是无论记录是否存在,都会以记录对象作为参数传递一个块,以进行更新。
    • 有点误导,不一定是答案,而是API。人们会期望该块无论如何都会通过,因此可以相应地创建/更新。相反,我们必须将其分解为单独的语句。嘘。
    【解决方案2】:

    在 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
    

    【讨论】:

    • assign_or_new 不会返回表中的第一行(如果存在)然后该行将得到更新?它似乎是为我做的。
    • User.where(email: "a@b.com").first 如果找不到,将返回 nil。确保你有一个where 范围
    • 只是说明updated_at不会被触及,因为assign_attributes被使用了
    • 它不会是你使用 assing_or_new 但如果你使用 update_or_create 因为保存会
    【解决方案3】:

    将此添加到您的模型中:

    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,则无法从类级别更新单个记录。
    • obj = self.find_or_create_by(args); obj.update(属性);返回 obj ;会工作。
    【解决方案4】:

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

    Model.upsert(column_name: value)
    

    对于多条记录,请使用upsert_all

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

    注意

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

    【讨论】:

    • 有人在生产中使用它吗?使用 6.0.33 Rails 无法按预期工作。如果我有记录 [#<Model id: 1, other_model_fk: 33, some_property: 'foo'>] 然后我们调用 Model.upsert(other_model_fk: 33, some_property: 'bar') 然后添加一个新行。 (顺便说一下,FK 列被索引了)。我们期望的行为“如果找到一个对象,它将更新它,否则它将创建一个新对象。” 似乎不起作用。它正在创建一个新对象。我在这里错过了什么?
    【解决方案5】:

    老问题,但为了完整起见,我将我的解决方案扔进了环中。 当我需要一个特定的查找但不存在时,我需要它。

    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
    

    【讨论】:

      【解决方案6】:

      你可以在这样的一个语句中做到这一点:

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

      【讨论】:

      • 这不起作用,因为它只会在找到原始记录时返回。 OP 要求提供一种始终更改值的解决方案,即使找到记录也是如此。
      【解决方案7】:

      sequel gem 添加了一个update_or_create 方法,似乎可以满足您的需求。

      【讨论】:

      • 问题是关于 Active Record。
      • 是的,question 是关于 AR 的,但这提供了另一种解决 problem 的方法。对于阅读此问题、有类似问题并且可能愿意使用 Sequel 的人来说,这可能会很有用。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-08-08
      • 1970-01-01
      • 2011-09-13
      • 1970-01-01
      • 2013-09-26
      • 1970-01-01
      相关资源
      最近更新 更多