martinrehfeld / role_model

RoleModel is the framework agnostic, efficient and declarative way to do user roles.
http://rdoc.info/github/martinrehfeld/role_model/master/frames
MIT License
324 stars 45 forks source link

Querying DB for particular roles?! #6

Closed gamov closed 13 years ago

gamov commented 13 years ago

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?

martinrehfeld commented 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.

gamov commented 13 years ago

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.

martinrehfeld commented 13 years ago

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.

gamov commented 13 years ago

Noted. I get back to you when I've looked deeper into this.

gamov commented 10 years ago

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?

martinrehfeld commented 10 years ago

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.

andrewhubbs commented 10 years ago

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.

gamov commented 10 years ago

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.