Schienen Magie erzeugen oder aktualisieren?

93

Ich habe eine Klasse namens CachedObject, die generische serialisierte Objekte speichert, die nach Schlüssel indiziert sind. Ich möchte, dass diese Klasse eine create_or_updateMethode implementiert . Wenn ein Objekt gefunden wird, wird es aktualisiert, andernfalls wird ein neues erstellt.

Gibt es eine Möglichkeit, dies in Rails zu tun, oder muss ich meine eigene Methode schreiben?

kid_drew
quelle

Antworten:

172

Schienen 6

Rails 6 fügte ein upsertund upsert_allMethoden hinzu, die diese Funktionalität bereitstellen.

Model.upsert(column_name: value)

[upsert] Es werden weder Modelle instanziiert noch Active Record-Rückrufe oder -Validierungen ausgelöst.

Schienen 5, 4 und 3

Nicht, wenn Sie nach einem Anweisungstyp "Upsert" suchen (bei dem die Datenbank ein Update oder eine Insert-Anweisung in derselben Operation ausführt). Rails und ActiveRecord verfügen standardmäßig über keine solche Funktion. Sie können jedoch den Upsert- Edelstein verwenden.

Andernfalls können Sie Folgendes verwenden: find_or_initialize_byoder find_or_create_by, die ähnliche Funktionen bieten, allerdings auf Kosten eines zusätzlichen Datenbanktreffers, der in den meisten Fällen kaum ein Problem darstellt. Wenn Sie also keine ernsthaften Leistungsprobleme haben, würde ich den Edelstein nicht verwenden.

Wenn beispielsweise kein Benutzer mit dem Namen "Roger" gefunden wird, wird eine neue Benutzerinstanz mit der nameEinstellung "Roger" instanziiert .

user = User.where(name: "Roger").first_or_initialize
user.email = "[email protected]"
user.save

Alternativ können Sie verwenden find_or_initialize_by.

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

In Schienen 3.

user = User.find_or_initialize_by_name("Roger")
user.email = "[email protected]"
user.save

Sie können einen Block verwenden, der Block wird jedoch nur ausgeführt, wenn der Datensatz neu ist .

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

Wenn Sie einen Block unabhängig von der Persistenz des Datensatzes verwenden möchten, verwenden Sie tapfür das Ergebnis:

User.where(name: "Roger").first_or_initialize.tap do |user|
  user.email = "[email protected]"
  user.save
end
Mohamad
quelle
1
Der Quellcode, auf den das Dokument verweist, zeigt, dass dies nicht so funktioniert, wie Sie es implizieren. Der Block wird nur dann an eine neue Methode übergeben, wenn der entsprechende Datensatz nicht vorhanden ist. In Rails scheint es keine "Upsert" -Magie zu geben - Sie müssen sie in zwei Ruby-Anweisungen aufteilen, eine für die Objektauswahl und eine für die Attributaktualisierung.
Sameers
@sameers Ich bin nicht sicher, ob ich verstehe, was du meinst. Was glaubst du, was ich damit meine?
Mohamad
1
Oh ... ich verstehe, was du jetzt gemeint hast - dass beide Formen find_or_initialize_byund find_or_create_byeinen Block akzeptieren. Ich dachte, Sie haben gemeint, dass unabhängig davon, ob der Datensatz vorhanden ist oder nicht, ein Block mit dem Datensatzobjekt als Argument übergeben wird, um die Aktualisierung durchzuführen.
Sameers
3
Es ist etwas irreführend, nicht unbedingt die Antwort, sondern die API. Man würde erwarten, dass der Block trotzdem übergeben wird und somit entsprechend erstellt / aktualisiert werden kann. Stattdessen müssen wir es in separate Aussagen aufteilen. Boo. <3 Rails obwohl :)
Volte
32

In Rails 4 können Sie einem bestimmten Modell hinzufügen:

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

und benutze es gerne

User.where(email: "[email protected]").update_or_create(name: "Mr A Bbb")

Oder wenn Sie diese Methoden lieber allen Modellen hinzufügen möchten, die in einen Initialisierer eingefügt wurden:

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
montrealmike
quelle
1
Wird assign_or_newdie erste Zeile in der Tabelle nicht zurückgegeben, wenn sie vorhanden ist, und diese Zeile wird dann aktualisiert? Es scheint das für mich zu tun.
Steve Klein
User.where(email: "[email protected]").firstwird nil zurückgeben, wenn nicht gefunden. Stellen Sie sicher , dass Sie eine haben whereUmfang
montrealmike
Nur eine Notiz zu sagen, updated_atdie nicht berührt wird, weil assign_attributesverwendet wird
lshepstone
Es wird nicht sein, dass Sie assing_or_new verwenden, sondern, wenn Sie update_or_create wegen des Speicherns verwenden
montrealmike
13

Fügen Sie dies Ihrem Modell hinzu:

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

Damit können Sie:

User.update_or_create_by({name: 'Joe'}, attributes)
Karen
quelle
Den zweiten Teil werde ich nicht arbeiten. Es kann kein einzelner Datensatz von der Klassenebene ohne die ID des zu aktualisierenden Datensatzes aktualisiert werden.
Aeramor
1
obj = self.find_or_create_by (args); obj.update (Attribute); return obj; wird funktionieren.
veeresh yh
12

Die Magie, nach der Sie gesucht haben, wurde hinzugefügt. Rails 6 Jetzt können Sie sie aktualisieren (aktualisieren oder einfügen). Für die Verwendung einzelner Datensätze:

Model.upsert(column_name: value)

Verwenden Sie für mehrere Datensätze upsert_all :

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

Hinweis :

  • Beide Methoden lösen keine Active Record-Rückrufe oder -Validierungen aus
  • unique_by => Nur PostgreSQL und SQLite
Imran Ahmad
quelle
1

Alte Frage, aber der Vollständigkeit halber meine Lösung in den Ring werfen. Ich brauchte dies, wenn ich einen bestimmten Fund brauchte, aber einen anderen, wenn er nicht existiert.

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
Aeramor
quelle
0

Sie können dies in einer Anweisung wie folgt tun:

CachedObject.where(key: "the given key").first_or_create! do |cached|
   cached.attribute1 = 'attribute value'
   cached.attribute2 = 'attribute value'
end
Rajesh Kolappakam
quelle
9
Dies funktioniert nicht, da der ursprüngliche Datensatz nur zurückgegeben wird, wenn einer gefunden wird. Das OP fragt nach einer Lösung, die den Wert immer ändert, auch wenn ein Datensatz gefunden wird.
JeanMertz