2010-08-26 8 views
51

Estoy utilizando Rails 'accepts_nested_attributes_for método con gran éxito, pero ¿cómo puedo tenerlo no crear nuevos registros si ya existe un registro?accept_nested_attributes_for con find_or_create?

A modo de ejemplo:

Di Tengo tres modelos, equipo, miembros, y el jugador, y cada uno de los jugadores del equipo has_many a través de membresías, y los jugadores pueden pertenecer a muchos equipos. El modelo de equipo podría aceptar atributos anidados 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 todavía no hay un jugador con el mismo nombre? Si hay es 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.

Respuesta

47

Cuando define un gancho para asociaciones de autoguardado, la ruta de código normal se omite y en su lugar se invoca su método. Por lo tanto, puede 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.

+0

¿Puede usted por favor señalar esta funcionalidad en la documentación? – dombesz

+4

También creo que el correcto es def autosave_associated_records_for_author. – dombesz

+1

¿Este método funciona del otro lado de la relación? por ejemplo, ¿qué pasa si tenemos has_many: authors? – dombesz

3

Sólo para redondear las cosas en términos de la cuestión (se refiere a find_or_create), el caso de bloque en respuesta Francois' podría ser reformulada como:

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

En rieles 4: self.author = Author.find_or_create_by (name: author.name) a menos que sea autor. name.blank? self.author.save! – Katarzyna

27

No piense en ello como la adición de jugadores a los equipos , piense en ello como agregar membresías a equipos. La forma no funciona con los jugadores directamente. El modelo de Membresía puede tener un atributo 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 a continuación, simplemente agregue un campo de texto player_name a cualquier creador de formularios de Membresía.

<%= f.text_field :player_name %> 

De esta manera no es específico de accepts_nested_attributes_for y se puede utilizar en cualquier forma membresía.

Nota: Con esta técnica, el modelo de jugador se crea antes de la validación. Si no desea este efecto, guarde el reproductor en una variable de instancia y guárdelo en una devolución de llamada before_save.

+3

Acerca de esa nota: si no quiere que el Jugador se cree antes de la validación, use 'find_or_initialize_by_name (name)' en lugar de 'find_or_create_by_name (name)' – Arcolye

+0

si pudiera votar esta respuesta un millón de veces. ¡Esto ha sido tan útil para relaciones complejas y para evitar la anidación profunda! ¡gracias! – Michelle

4

Al utilizar :accepts_nested_attributes_for, la presentación de la id de un registro existente hará que ActiveRecord a actualización el registro existente en lugar de crear un nuevo registro. No estoy seguro de lo que su margen de beneficio es similar, pero trata de algo más o menos así:

<%= 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 el id se suministra, pero crea lo contrario.

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

+0

Estoy buscando algo como esto para resolver mi problema. Pero parece incorrecto ya que un equipo tiene muchos jugadores. ¿Es realmente jugador y no jugadores? –

3

Esto funciona muy bien si tiene 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.

Configuración

Éstos son una versión básica de mis modelos:

Ubicación del objeto:

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 objetos

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

I , efectivamente reemplazar 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, borra y cambios toca al camino que necesitaba cuando se utiliza fields_for en una forma anidada. Estoy abierto a comentarios si hay formas de simplificar. Es importante señalar que estoy cambiando explícitamente las etiquetas cuando la etiqueta cambia en lugar de actualizar la etiqueta.

+0

Dustin ... Realmente aprecié este método. Está funcionando realmente bien para mí. Una pregunta: la rama '# Parece que estamos eliminando ...' no tiene sentido para mí. ¿En qué circunstancias un registro _no_ sería 'nuevo_registro?' Y tampoco tiene una ID? –

+1

@LannyBose puede suceder en solicitudes simultáneas – vemv

+0

@ dustin-m ¿cómo se ve la forma/vista de esto? Estoy intrigado porque me encontré con un problema casi idéntico http://stackoverflow.com/questions/37595050/rails-5-nested-forms-and-existing-associated-objects –

1

A before_validation gancho es una buena opción: es un mecanismo estándar que resulta en un código más simple que anulando el más oscuro autosave_associated_records_for_*. La respuesta de

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 
0

@ Dustin-m fue fundamental para mí - estoy haciendo algo personalizado con un has_many: a través de la relación. Tengo un tema que tiene una tendencia, que tiene muchos hijos (recursivo).

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

por lo que utiliza lo anterior para construir una copia de seguridad automática a medida 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

0

respuesta por @ François Beausoleil es impresionante y resuelto 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 el 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 #<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'> 

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 

Se debe a que, en el caso de la edición, self.author para el post ya habrá un autor con id: 1, el tope en otro sitio, bloquear y actualizará que author en lugar de crear uno nuevo.

he cambiado el código (elsif condición) 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 
Cuestiones relacionadas