Railsは魔法を作成または更新しますか?


93

CachedObjectキーでインデックス付けされた一般的なシリアル化されたオブジェクトを格納するというクラスがあります。このクラスにcreate_or_updateメソッドを実装してほしい。オブジェクトが見つかった場合は更新され、それ以外の場合は新しいオブジェクトが作成されます。

Railsでこれを行う方法はありますか、または独自のメソッドを記述する必要がありますか?

回答:


172

Rails 6

Rails 6では、この機能を提供するupsertおよびupsert_allメソッドが追加されました。

Model.upsert(column_name: value)

[upsert]モデルをインスタンス化したり、アクティブレコードのコールバックや検証をトリガーしたりすることはありません。

Rails 5、4、3

「アップサート」(データベースが同じ操作で更新または挿入ステートメントを実行する)タイプのステートメントを探している場合は除きます。そのままでは、RailsとActiveRecordにはそのような機能はありません。ただし、upsert gem を使用できます。

それ以外の場合は、以下を使用できます。find_or_initialize_byまたはfind_or_create_by、これは同様の機能を提供しますが、追加のデータベースヒットを犠牲にして行われますが、ほとんどの場合、問題はほとんどありません。したがって、パフォーマンスに深刻な懸念がない限り、私はこの宝石を使用しません。

たとえば、「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


1
ドキュメントが指すソースコードは、これが意図したとおりに機能しないことを示しています。対応するレコードが存在しない場合にのみ、ブロックが新しいメソッドに渡されます。Railsには「アップサート」の魔法がないようです。オブジェクトを選択するためと属性を更新するための2つのRubyステートメントに分ける必要があります。
2014

@sameersわかりません。私が何を意味していると思いますか?
Mohamad 2014

1
ああ...私はあなたが今何を意味しているのか分かります-それは両方の形をしていて、ブロックfind_or_initialize_byfind_or_create_by受け入れます レコードが存在するかどうかに関係なく、更新を行うために、レコードオブジェクトを引数としてブロックが渡されることを意味していると思いました。
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テーブルの最初の行が存在する場合はそれを返さず、その行が更新されますか?私にはそうしているようです。
スティーブクライン2015

User.where(email: "a@b.com").first見つからない場合はnilを返します。whereスコープがあることを確認してください
モントリオールマイク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)

私が働かない2番目の部分。更新するレコードのIDがないと、クラスレベルから単一のレコードを更新できません。
Aeramor

1
obj = self.find_or_create_by(args); obj.update(attributes); return obj;動作します。
veeresh yh 2017

12

あなたが探してきた魔法が追加されていRails 6 ますができます今すぐアップサート(更新または挿入)。単一レコード使用の場合:

Model.upsert(column_name: value)

複数のレコードの場合はupsert_allを使用します

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

  • どちらの方法もアクティブレコードのコールバックまたは検証をトリガーしません
  • 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

次のような1つのステートメントで実行できます。

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

9
元のレコードが見つかった場合にのみそれを返すため、これは機能しません。OPは、レコードが見つかった場合でも常に値を変更するソリューションを求めます。
JeanMertz 2014

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.