J'ai un ensemble de modèles HABTM assez 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
Maintenant, tout fonctionne bien, sauf que je reçois une tonne de doublons dans le tableau Tags.
Que dois-je faire pour éviter les doublons (bases sur le nom) dans la table des balises?
J'ai résolu ce problème en créant un filtre before_save qui corrige les problèmes.
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
Il pourrait être légèrement optimisé pour travailler par lots avec les tags, mais il pourrait également nécessiter un support transactionnel légèrement supérieur.
Les non suivants empêchent l’écriture de relations en double dans la base de données, elle garantit uniquement que les méthodes find
ignorent les doublons.
Dans Rails 5:
has_and_belongs_to_many :tags, -> { distinct }
Remarque: Relation#uniq
a été amorti dans Rails 5 ( commit )
_ {Dans Rails 4
has_and_belongs_to_many :tags, -> { uniq }
Option 1: Empêcher les doublons à partir du contrôleur:
post.tags << tag unless post.tags.include?(tag)
Cependant, plusieurs utilisateurs peuvent tenter post.tags.include?(tag)
en même temps, ce qui est soumis aux conditions de concurrence. Ceci est discuté ici .
Pour plus de robustesse, vous pouvez également ajouter ceci au modèle Post (post.rb).
def tag=(tag)
tags << tag unless tags.include?(tag)
end
Option 2: Créer un index unique
Le moyen le plus sûr d'éviter d'éviter les doublons est d'avoir des contraintes de doublon au niveau de la couche base de données. Ceci peut être réalisé en ajoutant un unique index
sur la table elle-même.
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
Une fois que vous disposez de l'index unique, toute tentative visant à ajouter un enregistrement en double provoquera une erreur ActiveRecord::RecordNotUnique
. Manipuler ceci est hors du champ de cette question. Voir cette SO question .
rescue_from ActiveRecord::RecordNotUnique, :with => :some_method
En plus des suggestions ci-dessus:
:uniq
à l'association has_and_belongs_to_many
Je voudrais faire une vérification explicite pour déterminer si la relation existe déjà. Par exemple:
post = Post.find(1)
tag = Tag.find(2)
post.tags << tag unless post.tags.include?(tag)
Dans Rails4:
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags, -> { uniq }
(attention, le -> { uniq }
doit être placé juste après le nom de la relation, avant les autres paramètres)
Vous pouvez passer l'option :uniq
comme décrit dans la documentation . Notez également que les options :uniq
n'empêchent pas la création de relations en double, mais s'assurent uniquement que les méthodes d'accès/de recherche les sélectionnent une fois.
Si vous souhaitez éviter les doublons dans la table d'association, vous devez créer un index unique et gérer l'exception. De plus, validates_uniqueness_of ne fonctionne pas comme prévu, car une deuxième demande est en train d'écrire dans la base de données entre le moment où la première demande vérifie les doublons et les écrit dans la base de données.
Définissez l'option 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
Je préférerais ajuster le modèle et créer les classes de cette façon:
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
J'encadrais ensuite la création dans une logique afin que les modèles de balises soient réutilisés s'ils existaient déjà. Je mettrais probablement même une contrainte unique sur le nom de la balise pour l'appliquer. Cela rend plus efficace les recherches dans les deux sens, car vous pouvez simplement utiliser les index de la table de jointure (pour rechercher toutes les publications d’une balise donnée et toutes les balises d’une publication particulière).
Le seul problème est que vous ne pouvez pas autoriser le changement de nom de balises car le changement de nom de balise affecterait toutes les utilisations de cette balise. Demandez à l'utilisateur de supprimer la balise et créez-en une nouvelle.
C'est vraiment vieux mais je pensais partager ma façon de faire.
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
end
Dans le code où je dois ajouter des tags à un message, je fais quelque chose comme:
new_tag = Tag.find_by(name: 'cool')
post.tag_ids = (post.tag_ids + [new_tag.id]).uniq
Cela a pour effet d'ajouter/de supprimer automatiquement les balises si nécessaire ou de ne rien faire si tel est le cas.
Pour moi travail
override << méthode dans la relation
has_and_belongs_to_many :groups do
def << (group)
group -= self if group.respond_to?(:to_a)
super group unless include?(group)
end
end
Extrayez le nom de la balise pour plus de sécurité. Vérifiez si la balise existe ou non dans votre table de balises, puis créez-la si ce n'est pas le cas:
name = params[:tag][:name]
@new_tag = Tag.where(name: name).first_or_create
Ensuite, vérifiez s’il existe dans cette collection spécifique et appuyez dessus si ce n’est pas le cas:
@taggable.tags << @new_tag unless @taggable.tags.exists?(@new_tag)
Vous devez ajouter un index sur la propriété tag: name, puis utiliser la méthode find_or_create dans la méthode Tags # create
Il suffit d’ajouter une coche à votre contrôleur avant d’ajouter l’enregistrement. Si c'est le cas, ne faites rien, sinon, ajoutez-en un nouveau:
u = current_user
a = @article
if u.articles.exists?(a)
else
u.articles << a
end
Plus: "4.4.1.14 collection.exists? (...)" http://edgeguides.rubyonrails.org/association_basics.html#scopes-for-has-and-belongs-to-many