I have a standard many-to-many relationship between users and roles in my Rails app:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, :through => :user_roles
end
I want to make sure that a user can only be assigned any role once. Any attempt to insert a duplicate should ignore the request, not throw an error or cause validation failure. What I really want to represent is a "set", where inserting an element that already exists in the set has no effect. {1,2,3} U {1} = {1,2,3}, not {1,1,2,3}.
I realize that I can do it like this:
user.roles << role unless user.roles.include?(role)
or by creating a wrapper method (e.g. add_to_roles(role)
), but I was hoping for some idiomatic way to make it automatic via the association, so that I can write:
user.roles << role # automatically checks roles.include?
and it just does the work for me. This way, I don't have to remember to check for dups or to use the custom method. Is there something in the framework I'm missing? I first thought the :uniq option to has_many would do it, but it's basically just "select distinct."
Is there a way to do this declaratively? If not, maybe by using an association extension?
Here's an example of how the default behavior fails:
>> 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">]
As long as the appended role is an ActiveRecord object, what you are doing:
user.roles << role
Should de-duplicate automatically for :has_many
associations.
For has_many :through
, try:
class User
has_many :roles, :through => :user_roles do
def <<(new_item)
super( Array(new_item) - proxy_association.owner.roles )
end
end
end
if super doesn't work, you may need to set up an alias_method_chain.