Les rails créent ou mettent à jour la magie?


93

J'ai une classe appelée CachedObject qui stocke des objets sérialisés génériques indexés par clé. Je veux que cette classe implémente une create_or_updateméthode. Si un objet est trouvé, il le mettra à jour, sinon il en créera un nouveau.

Existe-t-il un moyen de faire cela dans Rails ou dois-je écrire ma propre méthode?

Réponses:


172

Rails 6

Rails 6 a ajouté une méthode upsertet upsert_allqui offre cette fonctionnalité.

Model.upsert(column_name: value)

[upsert] Il n'instancie aucun modèle et ne déclenche pas de rappel ou de validation Active Record.

Rails 5, 4 et 3

Pas si vous recherchez un type d'instruction "upsert" (où la base de données exécute une mise à jour ou une instruction d'insertion dans la même opération). Hors de la boîte, Rails et ActiveRecord n'ont pas une telle fonctionnalité. Vous pouvez cependant utiliser la gemme upsert .

Sinon, vous pouvez utiliser: find_or_initialize_byou find_or_create_by, qui offrent des fonctionnalités similaires, mais au prix d'un accès supplémentaire à la base de données, ce qui, dans la plupart des cas, n'est pas du tout un problème. Donc, à moins que vous ayez de sérieux problèmes de performances, je n'utiliserais pas la gemme.

Par exemple, si aucun utilisateur n'est trouvé avec le nom "Roger", une nouvelle instance d'utilisateur est instanciée avec son nameréglage sur "Roger".

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

Vous pouvez également utiliser find_or_initialize_by.

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

Dans les rails 3.

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

Vous pouvez utiliser un bloc, mais le bloc ne s'exécute que si l'enregistrement est nouveau .

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

Si vous souhaitez utiliser un bloc quelle que soit la persistance de l'enregistrement, utilisez tapsur le résultat:

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


1
Le code source vers lequel pointe le document montre que cela ne fonctionne pas comme vous l'impliquez - le bloc est passé à une nouvelle méthode uniquement si l'enregistrement correspondant n'existe pas. Il ne semble pas y avoir de magie «upsert» dans Rails - vous devez le séparer en deux instructions Ruby, une pour la sélection d'objet et une pour la mise à jour des attributs.
sameers

@sameers Je ne suis pas sûr de comprendre ce que vous voulez dire. Que pensez-vous que j'implique?
Mohamad le

1
Oh ... je vois ce que tu voulais dire maintenant - que les deux se forment find_or_initialize_byet find_or_create_byacceptent un blocage. Je pensais que vous vouliez dire que, que l'enregistrement existe ou non, un bloc sera transmis avec l'objet d'enregistrement comme argument, afin de faire la mise à jour.
sameers

3
C'est légèrement trompeur, pas nécessairement la réponse, mais l'API. On s'attendrait à ce que le bloc soit passé indépendamment, et pourrait donc être créé / mis à jour en conséquence. Au lieu de cela, nous devons le diviser en déclarations séparées. Huer. <3 Rails cependant :)
Volte

32

Dans Rails 4, vous pouvez ajouter à un modèle spécifique:

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

et utilisez-le comme

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

Ou si vous préférez ajouter ces méthodes à tous les modèles mis dans un initialiseur:

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
Ne assign_or_newretournera pas la première ligne de la table si elle existe, puis cette ligne sera mise à jour? Il semble faire cela pour moi.
steve klein

User.where(email: "a@b.com").firstrenverra nul s'il n'est pas trouvé. Assurez-vous d'avoir une wherelunette
montrealmike

Juste une note pour dire que updated_atcela ne sera pas touché car il assign_attributesest utilisé
lshepstone

Ce ne sera pas si vous utilisez assing_or_new, mais si vous utilisez update_or_create à cause de la sauvegarde
montrealmike

13

Ajoutez ceci à votre modèle:

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

Avec cela, vous pouvez:

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

La deuxième partie, je ne travaillerai pas. Impossible de mettre à jour un seul enregistrement à partir du niveau de la classe sans l'ID de l'enregistrement à mettre à jour.
Aeramor

1
obj = self.find_or_create_by (args); obj.update (attributs); return obj; fonctionnera.
veeresh yh

12

La magie que vous recherchiez a été ajoutée dans Rails 6 Vous pouvez désormais insérer (mise à jour ou insertion). Pour un seul enregistrement:

Model.upsert(column_name: value)

Pour plusieurs enregistrements, utilisez upsert_all :

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

Remarque :

  • Les deux méthodes ne déclenchent pas les rappels ni les validations Active Record
  • unique_by => PostgreSQL et SQLite uniquement

1

Ancienne question mais jetant ma solution dans le ring pour l'exhaustivité. J'en avais besoin quand j'avais besoin d'une découverte spécifique mais d'une création différente si elle n'existe pas.

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

Vous pouvez le faire en une seule déclaration comme celle-ci:

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

9
Cela ne fonctionnera pas, car il ne retournera l'enregistrement d'origine que s'il en trouve un. L'OP demande une solution qui change toujours la valeur, même si un enregistrement est trouvé.
JeanMertz

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.