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.
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.
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.
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.
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.
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
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.
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
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!
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
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