Rails crea o aggiorna la magia?


93

Ho una classe chiamata CachedObjectche memorizza oggetti serializzati generici indicizzati per chiave. Voglio che questa classe implementi un create_or_updatemetodo. Se viene trovato un oggetto lo aggiornerà, altrimenti ne creerà uno nuovo.

C'è un modo per farlo in Rails o devo scrivere il mio metodo?

Risposte:


172

Rotaie 6

Rails 6 ha aggiunto metodi upserte upsert_allche forniscono questa funzionalità.

Model.upsert(column_name: value)

[upsert] Non istanzia alcun modello né attiva callback o convalide di Active Record.

Rotaie 5, 4 e 3

No, se stai cercando un tipo di istruzione "upsert" (dove il database esegue un aggiornamento o un'istruzione di inserimento nella stessa operazione). Fuori dagli schemi, Rails e ActiveRecord non hanno questa funzionalità. Tuttavia, puoi usare la gemma upsert .

Altrimenti, puoi usare: find_or_initialize_byor find_or_create_by, che offre funzionalità simili, anche se al costo di un ulteriore accesso al database, il che, nella maggior parte dei casi, non è affatto un problema. Quindi, a meno che tu non abbia seri problemi di prestazioni, non userei la gemma.

Ad esempio, se non viene trovato alcun utente con il nome "Roger", viene namecreata un'istanza di una nuova istanza utente con la relativa impostazione "Roger".

user = User.where(name: "Roger").first_or_initialize
user.email = "email@example.com"
user.save

In alternativa, puoi usare find_or_initialize_by.

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

In Rails 3.

user = User.find_or_initialize_by_name("Roger")
user.email = "email@example.com"
user.save

Puoi usare un blocco, ma il blocco viene eseguito solo se il record è nuovo .

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

Se vuoi usare un blocco indipendentemente dalla persistenza del record, usa tapsul risultato:

User.where(name: "Roger").first_or_initialize.tap do |user|
  user.email = "email@example.com"
  user.save
end


1
Il codice sorgente a cui punta il documento mostra che questo non funziona nel modo in cui lo implichi: il blocco viene passato a un nuovo metodo solo se il record corrispondente non esiste. Non sembra esserci alcuna magia "upsert" in Rails - devi separarlo in due istruzioni Ruby, una per la selezione degli oggetti e una per l'aggiornamento degli attributi.
Sameers

@sameers Non sono sicuro di aver capito cosa intendi. Cosa pensi che stia insinuando?
Mohamad

1
Oh ... capisco cosa intendevi ora - che entrambe le forme find_or_initialize_bye find_or_create_byaccettano un blocco. Pensavo volessi dire che, indipendentemente dal fatto che il record esista o meno, verrà passato un blocco con l'oggetto record come argomento, al fine di eseguire l'aggiornamento.
Sameers

3
È leggermente fuorviante, non necessariamente la risposta, ma l'API. Ci si aspetterebbe che il blocco venga passato a prescindere e quindi potrebbe essere creato / aggiornato di conseguenza. Invece, dobbiamo suddividerlo in dichiarazioni separate. Boo. <3 Rails però :)
Volte

32

In Rails 4 puoi aggiungere a un modello specifico:

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

e usalo come

User.where(email: "a@b.com").update_or_create(name: "Mr A Bbb")

O se preferisci aggiungere questi metodi a tutti i modelli inseriti in un inizializzatore:

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
Non assign_or_newrestituirà la prima riga nella tabella se esiste e quindi quella riga verrà aggiornata? Sembra che lo stia facendo per me.
steve klein

User.where(email: "a@b.com").firstrestituirà zero se non trovato. Assicurati di avere un whereambito
montrealmike

Solo una nota per dire che updated_atnon verrà toccata perché assign_attributesviene utilizzata
lshepstone

Non è che stai usando assing_or_new ma lo farai se usi update_or_create a causa del salvataggio
montrealmike

13

Aggiungi questo al tuo modello:

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

Con quello, puoi:

User.update_or_create_by({name: 'Joe'}, attributes)

La seconda parte non la lavorerò. Non è possibile aggiornare un singolo record dal livello di classe senza l'ID del record da aggiornare.
Aeramor

1
obj = self.find_or_create_by (args); obj.update (attributi); return obj; funzionerà.
veeresh yh

12

La magia che stavi cercando è stata aggiunta in Rails 6 Ora puoi eseguire l' upsert (aggiornare o inserire). Per uso singolo record:

Model.upsert(column_name: value)

Per più record usa upsert_all :

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

Nota :

  • Entrambi i metodi non attivano callback o convalide di Active Record
  • unique_by => Solo PostgreSQL e SQLite

1

Vecchia domanda ma gettando la mia soluzione sul ring per completezza. Ne avevo bisogno quando avevo bisogno di una ricerca specifica, ma una creazione diversa se non esiste.

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

Puoi farlo in una dichiarazione come questa:

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

9
Questo non funzionerà, poiché restituirà il record originale solo se ne viene trovato uno. L'OP chiede una soluzione che cambi sempre il valore, anche se viene trovato un record.
JeanMertz

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.