ruby on rails - rails - has_and_belongs_to_many, evitando engaños en la tabla de unión
rails foreign key (12)
Prevenir duplicados solo en la vista (solución diferida)
Lo siguiente no impide escribir relaciones duplicadas con la base de datos, solo garantiza que los métodos de find
ignoren los duplicados.
En Rails 5:
has_and_belongs_to_many :tags, -> { distinct }
Nota: la Relation#uniq
se depreció en Rails 5 ( commit )
En Rails 4
has_and_belongs_to_many :tags, -> { uniq }
Evite que se guarden datos duplicados (mejor solución)
Opción 1: evitar duplicados del controlador:
post.tags << tag unless post.tags.include?(tag)
Sin embargo, varios usuarios pueden intentar post.tags.include?(tag)
al mismo tiempo, por lo tanto, esto está sujeto a las condiciones de la carrera. Esto se discute here .
Para mayor robustez también puede agregar esto al modelo de publicación (post.rb)
def tag=(tag)
tags << tag unless tags.include?(tag)
end
Opción 2: crear un índice único
La forma más infalible de evitar duplicados es tener restricciones duplicadas en la capa de la base de datos. Esto se puede lograr agregando un unique index
en la tabla.
rails g migration add_index_to_posts
# migration file
add_index :posts_tags, [:post_id, :tag_id], :unique => true
add_index :posts_tags, :tag_id
Una vez que tenga el índice único, intentar agregar un registro duplicado generará un error ActiveRecord::RecordNotUnique
. Manejar esto está fuera del alcance de esta pregunta. Ver esta pregunta .
rescue_from ActiveRecord::RecordNotUnique, :with => :some_method
Tengo un conjunto de modelos HABTM bastante simple
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
def tags= (tag_list)
self.tags.clear
tag_list.strip.split('' '').each do
self.tags.build(:name => tag)
end
end
end
Ahora todo funciona bien, excepto que tengo un montón de duplicados en la tabla de etiquetas.
¿Qué debo hacer para evitar duplicados (bases en el nombre) en la tabla de etiquetas?
Además de las sugerencias anteriores:
- agregar
:uniq
a la asociaciónhas_and_belongs_to_many
- agregando índice único en la tabla de unión
Haría una comprobación explícita para determinar si la relación ya existe. Por ejemplo:
post = Post.find(1)
tag = Tag.find(2)
post.tags << tag unless post.tags.include?(tag)
Debería agregar un índice en la propiedad tag: name y luego usar el método find_or_create en el método Tags # create
En Rails4:
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags, -> { uniq }
(cuidado, el -> { uniq }
debe estar directamente después del nombre de la relación, antes de otros parámetros)
Establezca la opción de uniq:
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts , :uniq => true
end
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags , :uniq => true
Esto es muy viejo, pero pensé que compartiría mi manera de hacer esto.
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
end
En el código donde necesito agregar etiquetas a una publicación, hago algo como:
new_tag = Tag.find_by(name: ''cool'')
post.tag_ids = (post.tag_ids + [new_tag.id]).uniq
Esto tiene el efecto de agregar / eliminar etiquetas automáticamente según sea necesario o de no hacer nada si ese es el caso.
Extraiga el nombre de la etiqueta para seguridad. Verifique si la etiqueta existe o no en su tabla de etiquetas, luego créela si no:
name = params[:tag][:name]
@new_tag = Tag.where(name: name).first_or_create
Luego, verifique si existe dentro de esta colección específica, y presione si no:
@taggable.tags << @new_tag unless @taggable.tags.exists?(@new_tag)
Para mi trabajo
- agregando índice único en la tabla de unión
anula el << método en la relación
has_and_belongs_to_many :groups do def << (group) group -= self if group.respond_to?(:to_a) super group unless include?(group) end end
Preferiría ajustar el modelo y crear las clases de esta manera:
class Tag < ActiveRecord::Base
has_many :taggings
has_many :posts, :through => :taggings
end
class Post < ActiveRecord::Base
has_many :taggings
has_many :tags, :through => :taggings
end
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
Luego, envolvería la creación en lógica para que los modelos Tag se reutilizaran si ya existían. Probablemente incluso pondría una restricción única en el nombre de la etiqueta para hacerla cumplir. Eso hace que sea más eficiente buscar de cualquier manera, ya que puede usar los índices en la tabla de unión (para encontrar todas las publicaciones para una etiqueta en particular, y todas las etiquetas para una publicación en particular).
La única pega es que no puede permitir el cambio de nombre de las etiquetas, ya que cambiar el nombre de la etiqueta afectaría todos los usos de esa etiqueta. Haga que el usuario elimine la etiqueta y cree una nueva en su lugar.
Puede pasar la opción :uniq
como se describe en la documentación . También tenga en cuenta que :uniq
opciones de :uniq
no impiden la creación de relaciones duplicadas, solo garantiza que los métodos de acceso / búsqueda los seleccionarán una vez.
Si desea evitar duplicados en la tabla de asociación, debe crear un índice único y manejar la excepción. También validates_uniqueness_of no funciona como se esperaba porque puede caer en el caso de que una segunda solicitud escriba en la base de datos entre el momento en que la primera solicitud verifica si hay duplicados y escribe en la base de datos.
Simplemente agregue un cheque en su controlador antes de agregar el registro. Si lo hace, no haga nada, si no lo hace, agregue uno nuevo:
u = current_user
a = @article
if u.articles.exists?(a)
else
u.articles << a
end
Más información: "4.4.1.14 collection.exists? (...)" http://edgeguides.rubyonrails.org/association_basics.html#scopes-for-has-and-belongs-to-many
Trabajé al respecto creando un filtro before_save que corrige las cosas.
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
before_save :fix_tags
def tag_list= (tag_list)
self.tags.clear
tag_list.strip.split('' '').each do
self.tags.build(:name => tag)
end
end
def fix_tags
if self.tags.loaded?
new_tags = []
self.tags.each do |tag|
if existing = Tag.find_by_name(tag.name)
new_tags << existing
else
new_tags << tag
end
end
self.tags = new_tags
end
end
end
Se podría optimizar ligeramente para trabajar en lotes con las etiquetas, también puede necesitar un soporte transaccional ligeramente mejor.