Rails idiom pour éviter les doublons dans a beaucoup: à travers

j'ai une relation standard de plusieurs à plusieurs entre les utilisateurs et les rôles dans mon application Rails:

class User < ActiveRecord::Base
  has_many :user_roles
  has_many :roles, :through => :user_roles
end

je veux m'assurer qu'un utilisateur ne peut se voir attribuer n'importe quel rôle qu'une seule fois. Toute tentative d'insérer un duplicata doit ignorer la requête, et ne pas créer d'erreur ou d'échec de validation. Ce que je veux vraiment représenter est un "ensemble", où l'insertion d'un élément qui existe déjà dans l'ensemble n'a aucun effet. {1,2,3} U {1} = {1,2,3}, pas {1,1,2,3}.

je me rends compte que je peux le faire comme ceci:

user.roles << role unless user.roles.include?(role)

ou en créant une méthode d'enrubannage (par exemple add_to_roles(role) ), mais j'espérais un moyen idiomatique de le rendre automatique via l'association, de sorte que je puisse écrire:

user.roles << role  # automatically checks roles.include?

et il fait le travail pour moi. De cette façon, je n'ai pas à me rappeler de vérifier pour les dups ou d'utiliser la méthode personnalisée. Y a-t-il quelque chose dans le cadre qui me manque? J'ai d'abord pensé :uniq option pour has_many le ferait, mais c'est simplement "select distinct"."

y a-t-il un moyen de faire cette déclaration? Si non, peut-être en utilisant une extension d'association?

voici un exemple de la façon dont le comportement par défaut échoue:

    >> u = User.create
      User Create (0.6ms)   INSERT INTO "users" ("name") VALUES(NULL)
    => #<User id: 3, name: nil>
    >> u.roles << Role.first
      Role Load (0.5ms)   SELECT * FROM "roles" LIMIT 1
      UserRole Create (0.5ms)   INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3)
      Role Load (0.4ms)   SELECT "roles".* FROM "roles" INNER JOIN "user_roles" ON "roles".id = "user_roles".role_id WHERE (("user_roles".user_id = 3)) 
    => [#<Role id: 1, name: "1">]
    >> u.roles << Role.first
      Role Load (0.4ms)   SELECT * FROM "roles" LIMIT 1
      UserRole Create (0.5ms)   INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3)
    => [#<Role id: 1, name: "1">, #<Role id: 1, name: "1">]
35
demandé sur KingPong 2009-08-22 08:14:42

7 réponses

aussi longtemps que le rôle ajouté est un objet ActiveRecord, ce que vous faites:

user.roles << role
Le code

devrait être supprimé automatiquement pour les associations :has_many .

pour has_many :through , essayez:

class User
  has_many :roles, :through => :user_roles do
    def <<(new_item)
      super( Array(new_item) - proxy_association.owner.roles )
    end
  end
end

si super ne fonctionne pas, vous pouvez avoir besoin de configurer un alias_method_chain.

23
répondu austinfromboston 2012-12-17 05:08:39

utilisez la méthode |= Join.

vous pouvez utiliser la méthode de jointure" 151970920 |= 151980920 "pour ajouter un élément au tableau, à moins qu'il ne soit déjà présent. Assurez-vous d'envelopper l'élément dans un tableau.

role                  #=> #<Role id: 1, name: "1">

user.roles            #=> []

user.roles |= [role]  #=> [#<Role id: 1, name: "1">]

user.roles |= [role]  #=> [#<Role id: 1, name: "1">]

peut également être utilisé pour ajouter plusieurs éléments qui peuvent être ou ne pas être déjà présents:

role1                         #=> #<Role id: 1, name: "1">
role2                         #=> #<Role id: 2, name: "2">

user.roles                    #=> [#<Role id: 1, name: "1">]

user.roles |= [role1, role2]  #=> [#<Role id: 1, name: "1">, #<Role id: 2, name: "2">]

user.roles |= [role1, role2]  #=> [#<Role id: 1, name: "1">, #<Role id: 2, name: "2">]

a trouvé ceci technique sur ce StackOverflow réponse .

5
répondu Joshua Pinter 2018-03-22 18:04:18

vous pouvez utiliser une combinaison de validates_uniqueness_of et d'overriding << dans le modèle principal, bien que cela détecte également toute autre erreur de validation dans le modèle join.

validates_uniqueness_of :user_id, :scope => [:role_id]

class User
  has_many :roles, :through => :user_roles do
    def <<(*items)
      super(items) rescue ActiveRecord::RecordInvalid
    end
  end
end
3
répondu Pete Campbell 2011-03-16 19:01:15

je pense que la règle de validation appropriée est dans votre users_roles join model:

validates_uniqueness_of :user_id, :scope => [:role_id]
2
répondu bdon 2009-08-22 06:49:13

peut-être est-il possible de créer la règle de validation

validates_uniqueness_of :user_roles

puis attraper l'exception de validation et continuer avec élégance. Cependant, cela semble vraiment hacky et est très inélégant, si même possible.

0
répondu Schrockwell 2009-08-22 05:41:55

je pense que vous voulez faire quelque chose comme:

user.roles.find_or_create_by(role_id: role.id) # saves association to database
user.roles.find_or_initialize_by(role_id: role.id) # builds association to be saved later
0
répondu Richard Jones 2017-04-05 23:23:19

j'ai rencontré ceci aujourd'hui et j'ai fini par utiliser #remplacer , qui"va effectuer une diff et supprimer/ajouter seulement les enregistrements qui ont changé".

par conséquent, vous devez passer l'union des rôles existants (afin qu'ils ne soient pas supprimés) et votre nouveau rôle (s):

new_roles = [role]
user.roles.replace(user.roles | new_roles)

il est important de noter que cette réponse et celle acceptée chargent les objets associés roles dans la mémoire afin d'effectuer le Tableau diff ( - ) et union ( | ). Cela pourrait entraîner des problèmes de rendement si vous avez affaire à un grand nombre de documents connexes.

si c'est un problème, vous pouvez regarder dans les options qui vérifient l'existence via des requêtes d'abord, ou utiliser un type de requête INSERT ON DUPLICATE KEY UPDATE (mysql) pour l'insertion.

0
répondu Tyler Johnson 2017-04-25 18:16:27