66 votes

Has_and_belongs_to_many, évitant les doublons dans la table de liaison

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 

Maintenant, tout fonctionne bien sauf que j'obtiens un tas de doublons dans la table des Tags.

Que dois-je faire pour éviter les doublons (basés sur le nom) dans la table des tags ?

1 votes

Est-ce que vous voulez dire dupliquer dans la table de jointure (comme le suggère le titre) ou dans la table des tags ?

25voto

spyle Points 413

En plus des suggestions ci-dessus:

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

Je ferais 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)

3 votes

Solution propre. Dans votre modèle Post, ajoutez def tag=(tag); tags << tag unless tags.include?(tag); end pour la robustesse.

7 votes

Intéressant, la documentation sur l'association has_many (chercher include?) recommande de ne pas utiliser le unless ...include?() en raison de situations de concurrence. Je suppose que la même chose pourrait être vraie pour has_and_belongs_to_many.

0 votes

Est-ce que quelqu'un a une confirmation pour cela?

20voto

Simone Carletti Points 77653

Vous pouvez passer l'option :uniq comme décrit dans la documentation. Notez également que l'option :uniq ne prévient pas la création de relations en double, elle assure seulement que les méthodes accessor/find les sélectionneront une fois.

Si vous voulez éviter des doublons dans la table d'association, vous devriez créer un index unique et gérer l'exception. De plus, la validation validates_uniqueness_of ne fonctionne pas comme prévu car vous pourriez vous retrouver dans le cas où une deuxième requête écrit dans la base de données entre le moment où la première requête vérifie les doublons et inscrit dans la base de données.

0 votes

J'ai essayé cela, ça ne résout pas vraiment le problème de manière propre. Je veux éviter les exceptions dès le départ. Je pense que ma solution le fait un peu même si elle a besoin d'être ajustée.

2 votes

Note : J'avais déjà la contrainte unique, ce qui a entraîné des exceptions, les gérer est une grosse douleur.

1 votes

Avec la version actuelle de will_paginate (3.0.3), avoir des doublons dans la table de jointure rend impossible d'avoir une pagination unique.

20voto

nerith Points 826

Dans Rails4:

class Post < ActiveRecord::Base 
  has_and_belongs_to_many :tags, -> { uniq }

(attention, le -> { uniq } doit être directement après le nom de la relation, avant les autres paramètres)

Documentation Rails

0 votes

Mais le rappel after_add sera répété à chaque fois à nouveau

0 votes

Pouvez-vous élaborer sur cela? Voulez-vous dire que after_add sera toujours exécuté même dans le cas où un enregistrement n'est pas sauvegardé (grâce à {uniq})?

0 votes

@nerith Si j'ai une autre "has_and_belongs_to_many" dans l'autre modèle référençant le premier modèle, pensez-vous que je devrais aussi définir {uniq} là-bas?

12voto

Joshua Cheek Points 9450

Définir 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

4 votes

Cela ne fonctionne pas comme prévu. Vous obtenez toujours des erreurs de contraintes en double lorsque vous essayez de les ajouter via <<.

0 votes

Je viens d'essayer (sous Rails 2.3.5), et ce n'est pas vrai. Vous devriez poser votre propre question en fournissant suffisamment d'informations pour diagnostiquer votre problème.

15 votes

Je pensais que uniq ignorait les doublons lors de la lecture, mais permettait quand même d'écrire des doublons, ce qui provoquerait une exception de base de données si vous avez une contrainte de doublon au niveau de la base de données.

4voto

Sam Saffron Points 56236

J'ai contourné cela en créant un filtre before_save qui corrige les choses.

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, il pourrait également avoir besoin d'un meilleur support transactionnel.

0 votes

C'est exactement ce que fait le validateur validates_uniqueness_of. Cependant, cette solution seule ne prévient pas les éléments en double car entre votre find_by_name et l'association, une autre requête pourrait écrire dans la base de données. Vous pouvez utiliser ceci mais vous devez être conscient que certaines exceptions peuvent survenir (car vous avez un index) et vous devriez les attraper.

0 votes

Pas vraiment github.com/rails/rails/blob/… c'est une validation qui lancera une exception s'il y a un doublon, elle ne le gère pas de manière transparente, elle a des problèmes de concurrence documentés et doit être utilisée avec un index unique pour garantir l'absence de doublons.

0 votes

Oui, faites-le de cette façon et ajoutez également un index unique dans la base de données. Ensuite, plus tard, vous pourrez construire sur ce modèle et ajouter de nouvelles données, au cas où vous auriez besoin de stocker plus d'informations sur chaque association.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X