web-dev-qa-db-fra.com

accept_nested_attributes_for avec find_or_create?

J'utilise la méthode accept_nested_attributes_for de Rails avec un grand succès, mais comment puis-je l'avoir non créer de nouveaux enregistrements si un enregistrement existe déjà?

À titre d'exemple:

Supposons que j'ai trois modèles, Team, Membership, et Player, et que chaque équipe a_de nombreux joueurs par appartenance, et que les joueurs peuvent appartenir à plusieurs équipes. Le modèle Team peut alors accepter les attributs imbriqués pour les joueurs, mais cela signifie que chaque joueur soumis via le formulaire combiné équipe + joueur (s) sera créé (s) en tant que nouvel enregistrement de joueur.

Comment dois-je faire si je ne veux créer un nouveau disque de joueur que s'il n'y a pas déjà un joueur du même nom? Si il y a est un joueur portant le même nom, aucun nouvel enregistrement de joueur ne devrait être créé, mais le bon joueur devrait être trouvé et associé au nouvel enregistrement d’équipe.

52
trisignia

Lorsque vous définissez un point d'ancrage pour les associations d'enregistrement automatique, le chemin de code normal est ignoré et votre méthode est appelée à la place. Ainsi, vous pouvez faire ceci:

class Post < ActiveRecord::Base
  belongs_to :author, :autosave => true
  accepts_nested_attributes_for :author

  # If you need to validate the associated record, you can add a method like this:
  #     validate_associated_record_for_author
  def autosave_associated_records_for_author
    # Find or create the author by name
    if new_author = Author.find_by_name(author.name)
      self.author = new_author
    else
      self.author.save!
    end
  end
end

Ce code n'a pas été testé, mais il devrait correspondre exactement à ce dont vous avez besoin.

53

Ne pensez pas que cela signifie ajouter des joueurs à des équipes, mais que vous ajoutiez des membres à des équipes. Le formulaire ne fonctionne pas directement avec les joueurs. Le modèle d'appartenance peut avoir un attribut virtuel player_name. En coulisse, vous pouvez rechercher un joueur ou en créer un.

class Membership < ActiveRecord::Base
  def player_name
    player && player.name
  end

  def player_name=(name)
    self.player = Player.find_or_create_by_name(name) unless name.blank?
  end
end

Ensuite, ajoutez simplement un champ de texte player_name à n’importe quel générateur de formulaire d’adhésion.

<%= f.text_field :player_name %>

De cette façon, il n’est pas spécifique à includes_nested_attributes_for et peut être utilisé dans n’importe quel formulaire d’adhésion.

Remarque: avec cette technique, le modèle Player est créé avant la validation. Si vous ne souhaitez pas utiliser cet effet, stockez le lecteur dans une variable d'instance, puis enregistrez-le dans un rappel before_save.

30
ryanb

Lorsque vous utilisez :accepts_nested_attributes_for, la soumission de la id d'un enregistrement existant entraînera ActiveRecord dans update l'enregistrement existant au lieu de créer un nouvel enregistrement. Je ne sais pas à quoi ressemble votre balisage, mais essayez quelque chose comme ça:

<%= text_field_tag "team[player][name]", current_player.name %>
<%= hidden_field_tag "team[player][id]", current_player.id if current_player %>

Le nom du joueur sera mis à jour si la id est fournie, mais créée autrement.

L’approche consistant à définir la méthode autosave_associated_record_for_ est très intéressante. Je vais certainement utiliser ça! Cependant, considérez également cette solution plus simple.

4
Anson

Cela fonctionne très bien si vous avez une relation has_one ou apart_to. Mais a échoué avec un has_many ou has_many.

J'ai un système de marquage qui utilise une relation has_many: through. Aucune des solutions ici ne m'a conduit là où je devais aller, alors j'ai proposé une solution qui puisse aider les autres. Cela a été testé sur Rails 3.2.

Installer

Voici une version de base de mes modèles:

Objet de localisation:

class Location < ActiveRecord::Base
    has_many :city_taggables, :as => :city_taggable, :dependent => :destroy
    has_many :city_tags, :through => :city_taggables

    accepts_nested_attributes_for :city_tags, :reject_if => :all_blank, allow_destroy: true
end

Objets tag

class CityTaggable < ActiveRecord::Base
   belongs_to :city_tag
   belongs_to :city_taggable, :polymorphic => true
end

class CityTag < ActiveRecord::Base
   has_many :city_taggables, :dependent => :destroy
   has_many :ads, :through => :city_taggables
end

Solution

J'ai effectivement surchargé la méthode autosave_associated_recored_for comme suit:

class Location < ActiveRecord::Base
   private

   def autosave_associated_records_for_city_tags
     tags =[]
     #For Each Tag
     city_tags.each do |tag|
       #Destroy Tag if set to _destroy
       if tag._destroy
         #remove tag from object don't destroy the tag
         self.city_tags.delete(tag)
         next
       end

       #Check if the tag we are saving is new (no ID passed)
       if tag.new_record?
         #Find existing tag or use new tag if not found
         tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
       else
         #If tag being saved has an ID then it exists we want to see if the label has changed
         #We find the record and compare explicitly, this saves us when we are removing tags.
         existing = CityTag.find_by_id(tag.id)
         if existing    
           #Tag labels are different so we want to find or create a new tag (rather than updating the exiting tag label)
           if tag.label != existing.label
             self.city_tags.delete(tag)
             tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
           end
         else
           #Looks like we are removing the tag and need to delete it from this object
           self.city_tags.delete(tag)
           next
         end
       end
       tags << tag
     end
     #Iterate through tags and add to my Location unless they are already associated.
     tags.each do |tag|
       unless tag.in? self.city_tags
         self.city_tags << tag
       end
     end
   end

L’implémentation ci-dessus enregistre, supprime et modifie les étiquettes de la manière dont j’avais besoin lors de l’utilisation de fields_for dans un formulaire imbriqué. Je suis ouvert aux commentaires s’il existe des moyens de simplifier. Il est important de souligner que je change explicitement de balises lorsque l'étiquette change, plutôt que de mettre à jour l'étiquette.

3
Dustin M.

Un hook before_validation est un bon choix: il s'agit d'un mécanisme standard résultant en un code plus simple que de remplacer le autosave_associated_records_for_* plus obscur.

class Quux < ActiveRecord::Base

  has_and_belongs_to_many :foos
  accepts_nested_attributes_for :foos, reject_if: ->(object){ object[:value].blank? }
  before_validation :find_foos

  def find_foos
    self.foos = self.foos.map do |object|
      Foo.where(value: object.value).first_or_initialize
    end
  end

end
3
vemv

Juste pour compléter les choses en termes de question (fait référence à find_or_create), le bloc if dans la réponse de François pourrait être reformulé comme suit:

self.author = Author.find_or_create_by_name(author.name) unless author.name.blank?
self.author.save! 
3
KenB

La réponse de @ dustin-m a été déterminante pour moi - je fais quelque chose de traditionnel avec has_many: à travers les relations J'ai un sujet qui a une tendance, qui a beaucoup d'enfants (récursif). 

ActiveRecord n'aime pas quand je configure ceci comme une relation has_many :searches, through: trend, source: :children standard. Il récupère topic.trend et topic.searches mais ne fait pas topic.searches.create (name: foo). 

J'ai donc utilisé ce qui précède pour créer une sauvegarde automatique personnalisée et obtenir le résultat correct avec accepts_nested_attributes_for :searches, allow_destroy: true def autosave_associated_records_for_searches searches.each do | s | if s._destroy self.trend.children.delete(s) elsif s.new_record? self.trend.children << s else s.save end end end

0
David Hersey

Répondre par @ François Beausoleil est génial et a résolu un gros problème. Parfait pour en apprendre davantage sur le concept de autosave_associated_record_for

Cependant, j'ai trouvé un cas de coin dans cette implémentation. Dans le cas de update de l'auteur de la publication existante (A1), si un nouveau nom d'auteur (A2) est passé, le nom de l'auteur d'origine (A1) sera modifié. 

p = Post.first
p.author #<Author id: 1, name: 'JK Rowling'>
# now edit is triggered, and new author(non existing) is passed(e.g: Cal Newport).

p.author #<Author id: 1, name: 'Cal Newport'>

Code oral:

class Post < ActiveRecord::Base
  belongs_to :author, :autosave => true
  accepts_nested_attributes_for :author

  # If you need to validate the associated record, you can add a method like this:
  #     validate_associated_record_for_author
  def autosave_associated_records_for_author
    # Find or create the author by name
    if new_author = Author.find_by_name(author.name)
      self.author = new_author
    else
      self.author.save!
    end
  end
end

C'est parce que, en cas d'édition, self.author pour post sera déjà un auteur avec l'id: 1, il ira dans else, bloquera et mettra à jour cette author au lieu d'en créer un nouveau.

J'ai changé le code (elsif condition) pour atténuer ce problème:

class Post < ActiveRecord::Base
  belongs_to :author, :autosave => true
  accepts_nested_attributes_for :author

  # If you need to validate the associated record, you can add a method like this:
  #     validate_associated_record_for_author
  def autosave_associated_records_for_author
    # Find or create the author by name
    if new_author = Author.find_by_name(author.name)
      self.author = new_author
    elsif author && author.persisted? && author.changed?
      # New condition: if author is already allocated to post, but is changed, create a new author.
      self.author = Author.new(name: author.name)
    else
      # else create a new author
      self.author.save!
    end
  end
end
0
kiddorails