ruby-on-rails - validates - rails validations
accepts_nested_attributes_for con find_or_create? (8)
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 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 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.
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.
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 etiquetas 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 etiquetas cuando la etiqueta cambia en lugar de actualizar la etiqueta.
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
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 devolución de llamada before_save.
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!
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 #<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
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
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