accepts_nested_attributes_for con find_or_create?

Estoy utilizando el método accepted_nested_attributes_for de Rails con gran éxito, pero ¿cómo puedo evitar que cree nuevos registros si ya existe un registro?

A modo de ejemplo:

Digamos que tengo tres modelos, Equipo, Membresía y Jugador, y cada equipo tiene_muchos jugadores a través de membresías, y los jugadores pueden pertenecer a muchos equipos. El modelo de equipo podría aceptar atributos nesteds para jugadores, pero eso significa que cada jugador enviado a través del equipo combinado + jugador (s) se creará como un nuevo registro de jugador.

¿Cómo debo hacer las cosas si solo quiero crear un nuevo registro de jugador de esta manera si no hay un jugador con el mismo nombre? Si hay un jugador con el mismo nombre, no se deben crear nuevos registros de jugador, sino que se debe encontrar el jugador correcto y asociarlo con el nuevo récord del equipo.

Cuando define un gancho para asociaciones de autoguardado, la ruta de código normal se salta y en su lugar se invoca su método. Por lo tanto, puedes hacer esto:

 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 

Este código no ha sido probado, pero debería ser más o menos lo que necesita.

No piense que se trata de agregar jugadores a equipos, piense que se trata de agregar membresías a equipos. La forma no funciona con los jugadores directamente. El modelo de membresía puede tener un player_name virtual player_name . Detrás de escena, esto puede buscar a un jugador o crear uno.

 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 

Y luego simplemente agregue un campo de texto player_name a cualquier creador de formularios de Membresía.

 < %= f.text_field :player_name %> 

De esta forma, no es específico de accept_nested_attributes_for y se puede usar en cualquier formulario de membresía.

Nota: con esta técnica, el modelo Player se crea antes de que se realice la validación. Si no desea este efecto, guarde el reproductor en una variable de instancia y guárdelo en una callback before_save.

Cuando se utiliza :accepts_nested_attributes_for , el envío de la id de un registro existente hará que ActiveRecord actualice el registro existente en lugar de crear un nuevo registro. No estoy seguro de cómo es tu marcado, pero prueba algo como esto:

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

El nombre del jugador se actualizará si se proporciona el id , pero creado de otra manera.

El enfoque de definir autosave_associated_record_for_ method es muy interesante. ¡Ciertamente usaré eso! Sin embargo, considere esta solución más simple también.

Para redondear las cosas en términos de la pregunta (se refiere a find_or_create), el bloque if en la respuesta de Francois podría reformularse como:

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

Esto funciona muy bien si tienes una relación has_one o belongs_to. Pero se quedó corto con un has_many o has_many through.

Tengo un sistema de etiquetado que utiliza una relación has_many: through. Ninguna de las soluciones aquí me llevó a donde tenía que ir, así que se me ocurrió una solución que puede ayudar a otros. Esto ha sido probado en Rails 3.2.

Preparar

Aquí hay una versión básica de mis modelos:

Objeto de ubicación:

 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 

Tag Objects

 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 

Solución

De hecho, anulé el método autosave_associated_recored_for de la siguiente manera:

 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 

La implementación anterior guarda, elimina y cambia las tags de la forma que necesitaba al usar fields_for en forma anidada. Estoy abierto a comentarios si hay formas de simplificar. Es importante señalar que estoy cambiando explícitamente las tags cuando la etiqueta cambia en lugar de actualizar la etiqueta.

Un gancho before_validation es una buena opción: es un mecanismo estándar que da como resultado un código más simple que la anulación de los autosave_associated_records_for_* más oscuros.

 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 

La respuesta de @dustin-m fue instrumental para mí: estoy haciendo algo personalizado con una relación has_many: through. Tengo un tema que tiene una tendencia, que tiene muchos hijos (recursivo).

ActiveRecord no le gusta cuando configuro esto como un estándar has_many :searches, through: trend, source: :children relationship. Recupera topic.trend y topic.searches pero no hará topic.searches.create (name: foo).

Así que utilicé lo anterior para construir un autoguardado personalizado y estoy logrando el resultado correcto con 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 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

Respuesta de @ François Beausoleil es increíble y resolvió un gran problema. Genial para aprender sobre el concepto de autosave_associated_record_for .

Sin embargo, encontré un caso de esquina en esta implementación. En caso de update del autor de la publicación existente ( A1 ), si se pasa un nuevo nombre de autor ( A2 ), terminará cambiando el nombre del autor original ( A1 ).

 p = Post.first p.author # # now edit is triggered, and new author(non existing) is passed(eg: Cal Newport). p.author # 

Código Oringinal:

 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 

Es porque, en caso de edición, self.author para publicación ya será un autor con id: 1, irá en else, bloqueará y actualizará ese author lugar de crear uno nuevo.

Cambié el código (condición elsif ) para mitigar este problema:

 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