Rails 创造或更新魔法?

问题描述 投票:0回答:8

我有一个名为

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

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

ruby-on-rails activerecord
8个回答
227
投票

轨道6

Rails 6 添加了提供此功能的

upsert
upsert_all
方法。

Model.upsert(column_name: value)

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

轨道 5、4 和 3

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

否则,您可以使用:

find_or_initialize_by
find_or_create_by
,它们提供类似的功能,尽管代价是额外的数据库命中,在大多数情况下,这根本不是问题。因此,除非您有严重的性能问题,否则我不会使用 gem。

例如,如果没有找到名为“Roger”的用户,则会实例化一个新用户实例,并将其

name
设置为“Roger”。

user = User.where(name: "Roger").first_or_initialize
user.email = "[email protected]"
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 protected]"
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 protected]"
  user.save
end

34
投票

在 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: "[email protected]").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

15
投票

您一直在寻找的魔法已添加到

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

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)

7
投票

通过链接

find_or_initialize_by
update
,这可以通过一种简单的方式来实现,避免(根据我的经验,经常)不必要的
upsert
警告,并且还可以最大限度地减少数据库调用。

例如:

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

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

值得注意的是,如果您的

find_or_initialize_by
属性与多个 Class 实例匹配,则只会选择并更新“第一个”实例。


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

0
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.