¿Cómo evitar duplicados en una relación has_many: through?
-
11-07-2019 - |
Pregunta
¿Cómo puedo lograr lo siguiente? Tengo dos modelos (blogs y lectores) y una tabla JOIN que me permitirá tener una relación N: M entre ellos:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :blogs, :through => :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Lo que quiero hacer ahora es agregar lectores a diferentes blogs. La condición, sin embargo, es que solo puedo agregar un lector a un blog UNA VEZ. Por lo tanto, no debe haber duplicados (mismo readerID
, mismo blogID
) en la tabla BlogsReaders
. ¿Cómo puedo lograr esto?
La segunda pregunta es, ¿cómo obtengo una lista de blog a la que los lectores ya no están suscritos (por ejemplo, para completar una lista de selección desplegable, que luego se puede usar para agregar el lector a otro blog)?
Solución
¿Qué pasa con:
Blog.find(:all,
:conditions => ['id NOT IN (?)', the_reader.blog_ids])
¡Rails se encarga de la recopilación de identificadores para nosotros con métodos de asociación! :)
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ ClassMethods.html
Otros consejos
Solución más simple integrada en Rails:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers, :uniq => true
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :blogs, :through => :blogs_readers, :uniq => true
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Nota agregar la opción :uniq => true
a la llamada has_many
.
También es posible que desee considerar has_and_belongs_to_many
entre Blog y Reader, a menos que tenga otros atributos que le gustaría tener en el modelo de combinación (que actualmente no tiene). Ese método también tiene una :uniq
opción.
Tenga en cuenta que esto no le impide crear las entradas en la tabla, pero asegura que cuando consulta la colección obtiene solo uno de cada objeto.
Update
En Rails 4 la forma de hacerlo es a través de un bloque de alcance. Lo anterior cambia a.
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { uniq }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { uniq }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Actualización para Rails 5
El uso de uniq
en el bloque de alcance provocará un error NoMethodError: undefined method 'extensions' for []:Array
. Utilice distinct
en su lugar:
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { distinct }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Esto debería encargarse de su primera pregunta:
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
validates_uniqueness_of :reader_id, :scope => :blog_id
end
La vía 5.1 de Rails
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { distinct }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
La respuesta en este enlace muestra cómo anular " < < " método para lograr lo que está buscando sin generar excepciones o crear un método separado: Rails modismos para evitar duplicados en has_many: a través de
Estoy pensando que alguien vendrá con una mejor respuesta que esta.
the_reader = Reader.find(:first, :include => :blogs)
Blog.find(:all,
:conditions => ['id NOT IN (?)', the_reader.blogs.map(&:id)])
[editar]
Por favor, vea la respuesta de Josh a continuación. Es el camino a seguir. (Sabía que había una mejor manera de salir;)
La respuesta principal actualmente dice usar uniq
en el proceso:
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { uniq }, through: :blogs_readers
end
Sin embargo, esto patea la relación en una matriz y puede romper cosas que esperan realizar operaciones en una relación, no en una matriz.
Si usa distinct
lo mantiene como una relación:
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
La forma más fácil es serializar la relación en una matriz:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers
serialize :reader_ids, Array
end
Luego, al asignar valores a los lectores, los aplica como
blog.reader_ids = [1,2,3,4]
Al asignar relaciones de esta manera, los duplicados se eliminan automáticamente.