Closed gamov closed 13 years ago
DB persistence including querying is out of the scope of this Gem. A separate Role model with n:m association might work better for you.
I might be interested to try implementing this because it looks like SQL supports bitwise operator: http://www.functionx.com/sql/Lesson02c.htm Would it be helpful for the gem?
For my current application, I only have a manageable number of users so I can load them all in memory to find all belonging to a certain role.
I would like to keep RoleModel database agnostic. But if you need any helper methods to facilitate the query generation, we could decide, if anything should be added to the RoleModel interface (if it is generic enough).
Also, I would like to point you to the work of Kristian Mandrup https://github.com/kristianmandrup. His various roles_* repos seem to tackle similar problems.
Noted. I get back to you when I've looked deeper into this.
Ok, when the user base grows, querying for roles becomes a performance issue. I think the two following methods would go a long way before starting creating more tables (has_many) etc.
RoleModel#mask_value_for_roles roles_array Return an array with all the possible values of the mask containing the roles passed. Useful to query a DB like this:
SELECT * FROM users WHERE roles_mask IN (values)
Another one would be:
def self.mask_has_any_roles mask, *roles
roles.any?{|role| (mask & 2**User.valid_roles.index(role)).nonzero?}
end
Returns a boolean indicating if any of the roles passed are present in the mask Useful when doing a straight SELECT :id, :role_mask from the DB and not instancing an ActiveRecord object.
What do you think?
I would like to point you to prior discussion on this approach in #25 and #24. Specifically, maybe you could get in touch with @andrewhubbs and see how he solved it. I think it would still be a good idea to make this functionality an addon (I would gladly link to it), but rather not include it in RoleModel.
The bad news is that I used "dirty bit masking". The good news is it is actually quite easy to understand. It makes sense and it perfectly reasonable to do when you have only a few roles. But if you have more than say 5 or 6 it gets unwieldy (which is why I totally agree with @martinrehfeld that it should not be included directly in RoleModel
). Below is an example of how I did it.
class User < ActiveRecord::Base
include RoleModel
ROLES = %w(buyer seller auctioneer observer admin)
roles *ROLES
def self.roles_for(mask_value)
ROLES.reject { |r| ((mask_value || 0) & 2**ROLES.index(r)).zero? }
end
def self.generate_mask_values(role)
sig_bit = 2**ROLES.index(role)
(0..64).map do |i|
(i & sig_bit).zero? ? nil : i
end.compact
end
ROLES.each do |role|
define_method :"#{role}?" do
is?(role)
end
scope role, where(roles_mask: self.generate_mask_values(role))
end
end
Now with that setup I can do something like the following:
@user.buyer?
=> false
or
@team = Team.find(1)
@team.users.buyer
User Load (8.0ms) SELECT "users".* FROM "users" WHERE "users"."team_id" = 1 AND "users"."roles_mask" IN (1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63)
I hope that helps. Let me know if you have any questions. This is something that could be pulled out into an addon as Martin suggested. If there is interest I can do that.
Thanks everyone for your contribution! I'll roll with @andrewhubbs implementation. I tried with 10 roles and it's indeed … unwieldy! It seems I didn't do my homework well enough and should I have found @andrewhubbs pull request.
Maybe there is something I'm not getting.... Using Rails, I've defined some users with the role 'buyer'. My issue now is how do I get all the users with role 'buyer' from the DB via ActiveRecord? Do I need to do some dirty bitmasking or the gem proposes something like scopes?