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">]
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.
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 .
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
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]
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.
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
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.