web-dev-qa-db-fra.com

has_and_belongs_to_many, en évitant les dupes dans la table de jointure

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? 

54
Sam Saffron

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. 

4
Sam Saffron

Empêcher les doublons dans la vue uniquement (solution différée)

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 }

Empêcher les doublons données d'être enregistrées (meilleure solution)

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
35
Jeremy Lynch

En plus des suggestions ci-dessus:

  1. ajouter :uniq à l'association has_and_belongs_to_many 
  2. ajout d'index unique sur la table de jointure

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)
24
spyle

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)

Documentation Rails

21
cyrilchampier

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.

20
Simone Carletti

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
13
Joshua Cheek

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.

5
Jeff Whitmire

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.

2
Javeed

Pour moi travail

  1. ajout d'index unique sur la table de jointure
  2. 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
    
2

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)
1
dav1dhunt

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

docs

0
ajbraus

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

0
Matthew Bennett