121 votes

Rails crée ou met à jour la magie ?

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

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

226voto

Mohamad Points 8497

Rails 6

Rails 6 a ajouté un upsert y upsert_all qui fournissent cette fonctionnalité.

Model.upsert(column_name: value)

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

Rails 5, 4 et 3

Pas si vous recherchez une instruction de type "upsert" (lorsque la base de données exécute une mise à jour ou une insertion au cours de la même opération). Rails et ActiveRecord ne disposent pas d'une telle fonctionnalité. Vous pouvez utiliser la fonction upsert un joyau, cependant.

Sinon, vous pouvez utiliser : find_or_initialize_by ou find_or_create_by qui offrent une fonctionnalité similaire, mais au prix d'un accès supplémentaire à la base de données, ce qui, dans la plupart des cas, n'est guère un problème. Donc, à moins que vous n'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 nom name est réglé sur "Roger".

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

Alternativement, vous pouvez utiliser find_or_initialize_by .

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

Dans 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 voulez utiliser un bloc indépendamment de la persistance de l'enregistrement, utilisez tap sur le résultat :

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

0 votes

1 votes

Le code source auquel renvoie le document montre que cela ne fonctionne pas comme vous le laissez entendre - le bloc n'est transmis à une nouvelle méthode que si l'enregistrement correspondant n'existe pas. Il semble qu'il n'y ait pas de magie "upsert" dans Rails - vous devez la séparer en deux déclarations Ruby, une pour la sélection de l'objet et une pour la mise à jour de l'attribut.

0 votes

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

34voto

montrealmike Points 3795

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 l'utiliser 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, mettez un initialisateur :

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 votes

Ne le fera pas assign_or_new renvoie la première ligne du tableau si elle existe, puis cette ligne sera mise à jour ? C'est ce qu'il semble faire pour moi.

0 votes

User.where(email: "a@b.com").first renverra nil s'il n'est pas trouvé. Assurez-vous d'avoir un where champ d'application

0 votes

Juste une note pour dire que updated_at ne sera pas touchée parce que assign_attributes est utilisé

15voto

Imran Ahmad Points 4242

La magie que vous recherchiez a été ajoutée en Rails 6 Maintenant vous pouvez upsert (mise à jour ou insertion). Pour un seul enregistrement :

Model.upsert(column_name: value)

Pour les enregistrements multiples, utilisez upsert_all :

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

Note :

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

1 votes

Quelqu'un l'utilise-t-il en production ? Cela ne fonctionne pas comme prévu pour moi qui utilise la version 6.0.33 de Rails. Si j'ai un enregistrement [#<Model id: 1, other_model_fk: 33, some_property: 'foo'>] puis nous appelons Model.upsert(other_model_fk: 33, some_property: 'bar') puis une nouvelle ligne est ajoutée. (la colonne FK est d'ailleurs indexée). Le comportement que nous attendons "Si un objet est trouvé, il le met à jour, sinon il en crée un nouveau. ne semble pas fonctionner. Il crée un nouvel objet. Qu'est-ce qui m'échappe ?

13voto

Karen Points 751

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 ça, vous pouvez :

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

0 votes

La deuxième partie ne fonctionnera pas. Il est impossible de mettre à jour un seul enregistrement à partir du niveau de la classe sans l'ID de l'enregistrement à mettre à jour.

2 votes

Obj = self.find_or_create_by(args) ; obj.update(attributes) ; return obj ;fonctionnera.

1voto

Aeramor Points 81

C'est une vieille question, mais j'y ajoute ma solution pour être complet. J'avais besoin de cette solution lorsque j'avais besoin d'une recherche 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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X