#189
Nov 23, 2009

Embedded Association

Learn how to set up a one-to-many or many-to-many association which is entirely embedded into a single column through a string or bitmask.
Download (18.7 MB, 14:03)
alternative download for iPod & Apple TV (15.4 MB, 14:03)

Resources

One to Many

script/generate migration add_role_to_users role:string
rake db:migrate
# models/user.rb
class User < ActiveRecord::Base
  acts_as_authentic
  has_many :articles
  has_many :comments
  
  ROLES = %w[admin moderator author]
  
  def role_symbols
    [role.to_sym]
  end
end
<!-- users/new.html.erb -->
<p>
  <%= f.label :role %><br />
  <%= f.collection_select :role, User::ROLES, :to_s, :titleize %>
</p>

Many to Many

script/generate migration add_roles_mask_to_users roles_mask:integer
rake db:migrate
# models/user.rb
class User < ActiveRecord::Base
  acts_as_authentic
  has_many :articles
  has_many :comments
  
  named_scope :with_role, lambda { |role| {:conditions => "roles_mask & #{2**ROLES.index(role.to_s)} > 0"} }
  
  ROLES = %w[admin moderator author]
  
  def roles=(roles)
    self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.sum
  end
  
  def roles
    ROLES.reject { |r| ((roles_mask || 0) & 2**ROLES.index(r)).zero? }
  end
  
  def role_symbols
    roles.map(&:to_sym)
  end
end
<!-- users/new.html.erb -->
<p>
  <%= f.label :roles %><br />
  <% for role in User::ROLES %>
    <%= check_box_tag "user[roles][]", role, @user.roles.include?(role) %>
    <%=h role.humanize %><br />
  <% end %>
  <%= hidden_field_tag "user[roles][]", "" %>
</p>

RSS Feed for Episode Comments 58 comments

1. Ryan Oberholzer Nov 23, 2009 at 00:08

Awesome been looking forward to this one!


2. Igor Nov 23, 2009 at 00:59

Great one, thanks a lot!


3. QuBiT Nov 23, 2009 at 01:15

Thx @ ryan for showing us other ways to get things done.

BUT in my opinion there is another big risk in your last approach when you are using the role_mask approach:

What if you want to remove a role from your system? you can remove the definition from authorization_rules.rb, but when you remove the name from the array, then users assigned to the "old" role, will get assigned to the next role, which may be not allowed for them.

Additional: what about active and inactive roles? normal users may subscribe to "admin" but they should not be allowed to.

The working solution for me:
I've choosen to store roles inside the db: roles and role_assignments where role_assignments has an "active" field which has to be set by another action only allowed by admins. it is very easy to modify the code from the last railscast to use only active_roles (association) inside the user model. Having your roles and assignments inside the db, this enables you to add and remove roles easily as you only have to modify config/authorization_rules.rb to enable them (you do NOT have to change anything else in your code).

there is many more you can do: foreign_key constraints, ...
to enforce your strategy.

AND don't forget to write tests ^^

so thx 4 the tutorial on how different one problem can be "solved" and keep gooing ^^


4. egarcia Nov 23, 2009 at 01:17

I liked this one, specially the collection_select on the single-role part.

It is worth pointing out that declarative_authorization allows roles to "inherit" from other roles, so if you are using it there's a chance you can just have one role per user.


5. Jonathan L. Nov 23, 2009 at 01:31

Thanks a lot Ryan!
I've been using serialize lately and searching for matching records was such a pain :)


6. Benjamin Lewis Nov 23, 2009 at 03:07

I've got UG...

Just kidding, thanks a lot Ryan! Keep up the great work.

Thanks
Ben


7. Pfft Nov 23, 2009 at 03:21

It's suprising how many devs (noobs?) don't know what are or how to use bitmasks. It's a very common technique to store status flags.


8. pulkit Nov 23, 2009 at 03:33

nice screen cast and nice to see no spams (YET!!!)


9. David William Nov 23, 2009 at 05:00

Once again, thanks a lot. What a bonzer help!! I used to get headcase about many to many associations but lately everything is so much better.

Useful information, as usual.


10. David Backeus Nov 23, 2009 at 05:18

I would probably go for a comma separated approach myself. And use a "role like 'admin'" query if I need to fetch all admin users etc.

In my mind that would make everything more obvious both in looking at the code and the database.

Still interesting to learn about the bitmask approach though!


11. David Backeus Nov 23, 2009 at 05:19

Make that "role like '%admin%'".


12. Jurgen Nov 23, 2009 at 06:08

Ryan, you're my role model ;-}


13. Shreyans Nov 23, 2009 at 07:13

@Ryan, thanks a lot for introducing me the bitmask concept. Definitely I will use it in my project.


14. Simon Nov 23, 2009 at 07:26

Way too much code for this simple little thing. I don't really see the improvement in contrast to the added amount of code, which makes it more confusing if you don't know it.
This must be documented more carefully and so on and so on.

Of course it's a common approach to use bitmasks, but sometimes it's way too much.
Have fun maintaining (and especially changing) this...

KISS ;)
Simon


15. Dobo Nov 23, 2009 at 09:47

Thanks!


16. Joseph Silvashy Nov 23, 2009 at 09:54

I'd be afraid of this making my relationships too complex. I'll give it a try!


17. tow8ie Nov 23, 2009 at 12:52

Have a look at the FlagShihTzu plugin that handles all these things for you:

http://github.com/xing/flag_shih_tzu


18. Aukan Nov 23, 2009 at 13:05

Wonderful! I was actually wondering if this was posible. Thanks a lot.


19. Michael Gee Nov 23, 2009 at 14:03

I agree that you had tight coupling between authorization_rules.rb and the list of roles in the DB, but I was hoping you would go the other way and show us a way to define the allowed actions of each role in the DB, too. Thanks


20. Ryan Bates Nov 23, 2009 at 14:33

@QuBiT, you're right, removing roles can be difficult. If you need that functionality it's possible to change the roles array into a 2 dimensional array containing a static id integer. This way one wouldn't need to rely on the index position of each role.

As for your other solution of using active/inactive roles. This sounds like a role based solution which is different than the one I'm presenter here. If it is the same I'd love to see example of the code involved since I'm not certain how it would work without the tight coupling between code and database.

@David, the database matching of a comma-separated list doesn't feel as precise to me, but I can see the implementation being simpler. Thanks for the idea!

@Simon, there isn't too much code involved here: about 7 lines which replaces two models, their associations, and two database tables. Not to mention removing the coupling between the database and the code.

It really doesn't feel right to have database records so tightly coupled with code. One would need to ensure that database table is always the same in every environment (development, test and production).

I agree a bitmask can be complex, but here you shouldn't have to mess with it, and you can abstract it away more if you find the need. The underlying logic behind the alternative many-to-many assignment (role_ids= method) is actually much more complex than what we're doing here.

However, if you have an alternative solution I'd love to hear it.

@Michael, Often authorization rules are complex and difficult to map entirely in a database. However, if the authorization rules are simple enough to define in the database then keeping the roles there would make sense.

The technique I'm showing here definitely doesn't apply to every role based authorization solution. Instead it's a generic way to deal with tight coupling between the database and code.


21. Samuel Lebeau Nov 23, 2009 at 17:15

Great job as usual, thanks Ryan !

It's good to see good old bitmasks.

I'll add a little improvement: ActiveSupport's `Array#sum` takes a block and the bitshift operator is faster than the exponentiation one, so a nicer way to handle this could be http://gist.github.com/241532


22. Matt Powell Nov 23, 2009 at 17:28

Here's a neat trick that works with your code to add magic methods like "user.admin?":

http://gist.github.com/241546


23. Johan Nov 23, 2009 at 20:04

I just love bitmasks


24. Nami Nov 24, 2009 at 05:51

No roles ... (don't work:
in 188, I were having no roles
now, I have error in logout & it doesn't work (I can select roles, but I do nothing ...))


25. Rick DeNatale Nov 24, 2009 at 09:32

Ryan,

Another solution to the issue raised by @QuBiT might be to delete a role by replacing the name with a placeholder string, like -deleted-

Of course the code would be a bit more complicated since you wouldnt want the -deleted- roles to render checkboxes, but I'd probably move that logic from the view to a helper method anyway.


26. Bret Nov 24, 2009 at 10:29

I ran into a problem trying to combine this technique (using Many-to-Many) with formtastic and multi-model forms (using the complex forms example for 2.3 that you have out on github).

My code for the partial that is not saving correctly looks like this:

http://pastie.org/713110

When I was using a string field to hold this information, the form worked fine. Now it displays correctly (the correct checkboxes are checked) but, when I edit and try to save, the changes to the days are not recorded.

Here Showtime::DAYS is collection of weekdays (Mon - Fri) and I using a days_mask in my database.

Since I followed your tutorials for complex forms, formtastic and now this to the letter (changing only the field names to suit), I thought you might be uniquely able to discern why the new days don't save in the complex form.

As you can tell, I'm a BIG fan!


27. Bret Nov 24, 2009 at 11:11

Well that makes it twice today I wrote without looking. I must tired and I sincerely apologize for the lousy posts.

My code works fine once I opened my eyes, saw the 'protected' error and realized this is the one model where I hadn't update the attr_accessible.

To end on a positive note, it is good to know that, by combining your techniques, I can create nice DRY complex forms using appropriate models and associations.

Thanks Ryan!


28. limeyd Nov 24, 2009 at 15:54

Awesome cast as usual Ryan.

My tuppence - I think that roles do belong in a separate table and with associations to actions,
the actions(create, edit, post review, delete etc...) are application not the roles these are arbitrary. Thoughts?

Anyway I do think the mask idea is cool concept and would be fine for such a simple Auth app.


29. Squiddhartha Nov 24, 2009 at 16:09

For this kind of situation I've just used individual Boolean attributes on the models -- sure, you end up with a bunch of attributes, but they're all independent so you can add or remove them as desired. Though you do then need to worry about keeping "attr_accessible" up to date...


30. Bernhard Nov 24, 2009 at 16:10

You are my rails hero. I'm somehow a beginner and your topics can be challenge, but they are far the best and most entertaining way for me to learn RoR.

Thanks!!


31. Marc-Andre Lafortune Nov 25, 2009 at 10:08

Note: Using bitshift is better and more readable. Did you know you there was a bit reference built-in?

fixnum[bit] => 0 or 1, so:

ROLES.reject { |r| (roles_mask || 0)[ROLES.index(r)].zero? }


32. Steven Fines Nov 25, 2009 at 12:14

I love the content on the site, it's been a great help in learning rails.

As for the spam problem, you might want to just integrate askimet via
http://github.com/jfrench/rakismet and let it do the work of distinguishing between the ham and spam.


33. jason Nov 26, 2009 at 08:32

How do you use this with nested fields for? when I use the check_box_tag I can't find what to pass it (first parameter) to make it create the name correctly:

here is an example of the name:
family[person_attributes][0][registration_attributes][0][weeknights][1]


34. gene Nov 26, 2009 at 12:20

I am sure this question has been asked before, however I can't find an answer for it: how do you get model print as tables in the rails console?

I really love that feature!


35. spochtsfroind Nov 27, 2009 at 03:06

@gene: The tool for irb is called 'hirb' and could be found here:
http://github.com/cldwalker/hirb/tree/master


36. Matt Werner Nov 28, 2009 at 13:31

Hey Ryan, just a small improvement to your roles= method:

Wrap roles up to allow non-array assignment

self.roles_mask = ([*roles] & ROLES).map { |r| 2**ROLES.index(r) }.sum

That way you can just pass user.roles = 'admin'


37. Sam Millar Nov 29, 2009 at 15:41

These sort of casts really interest me as I am learning about something I had no idea about before.

However I am sure I have a very good use for bitmasks now in my upcoming app.

Thanks! :)


38. gamal99 Dec 02, 2009 at 05:25

Hello,

what kind of Plugin is it, to wrtie "Role.all" and you get lovely formattet answer?


39. Leo Wong Dec 05, 2009 at 03:38

Thanks Ryan! I just merged your smart code into my new project. It runs smoothly on my development machine, like always. But when staging on Heroku, the server emits errors showing that I have to convert `roles_mask' to integer before it will work as expected:

  ROLES.reject { |r| ((roles_mask.to_i || 0) & 2**ROLES.index(r)).zero? }

It's strange, and I don't know why. Since `roles_mask' is an integer field in the database, ActiveRecord could automatically detect its type and assign the value to an integer variable accordingly. Am I right or did I miss something?


40. Juan Dec 07, 2009 at 18:51

How can i make this
<%= f.collection_select :role, User::ROLES, :to_s, :titleize %>
In formtastic? The part for :humanize or :titleize specially.

Thanks!


41. vlad Dec 16, 2009 at 07:43

Hi

How you get formatted Role.all ?


42. Peter Marreck Dec 16, 2009 at 11:11

I believe that the named_scope technique here (which pushes the bit math out to the database to do the filtering on) will not work on MySQL, as MySQL bit arithmetic only encompasses TRUE and FALSE values and not all numbers, like it should. =/ Just an FYI (and also can anyone confirm?)


43. Peter Marreck Dec 16, 2009 at 12:04

Nevermind. I was confusing logical (example, &&) with bitwise (example, &) operators in MySQL after having read this http://dev.mysql.com/doc/refman/5.0/en/logical-operators.html

So it appears all the popular DB's do things like "SELECT 1 | 4" to return 5 (101). Nice.


44. tina Dec 18, 2009 at 17:37

http://www.footwearkicks.com


45. Sigi Dec 20, 2009 at 03:48

"Not to mention removing the coupling between the database and the code."

You keep mentioning this over and over, but I don't see how the bitmask approach removes the coupling. All it does is introduce a different kind of coupling, one which is much harder to see than going to a join model and roles table: the precise number that contains the bitmask is COMPLETELY coupled with the array entries inside the ROLES constant.

Changing the latter breaks everything, unless you never remove an entry and only strictly append new entries to the end (you mention that).

This is one of those "clever" solutions that will only cause you pain later on when you've forgotten what you did and why (or your teammates will break it for you :-).


47. Amiel Martin Dec 22, 2009 at 18:22

also check out bitmask_attributes: http://github.com/amiel/bitmask_attributes


47. Nike basketball shoes Dec 23, 2009 at 00:01

This is one of those "clever" solutions that will only cause


48. 琳琳的小狗 Dec 23, 2009 at 01:00

ROLES.index(role) mabye return nil,so:

def roles=(roles)
    self.roles_mask = roles.inject(0) do |bit, role|
      1 << (ROLES.index(role) || -1) | bit
    end
  end
  
  def roles
    ROLES.select { |role| 1 << ROLES.index(role) & self.roles_mask > 0 }
  end
  
  named_scope :with_role, lambda { |role| {:conditions => "roles_mask & #{1 << (ROLES.index(role.to_s) || -1)} > 0"} }


49. zirconium Dec 28, 2009 at 17:23

I can understand one-to-many associastion clearly but cannot many-to-many association. Can you interpret it for me? Thank you.


50. cutie Dec 28, 2009 at 22:40

Great ,thanks a lot!


51. yeni müzik Dec 29, 2009 at 15:45

This is one of those "clever" solutions that will only cause


52. roberth Dec 30, 2009 at 19:24

For the life of me I cannot get the many to many association to work. The roles for each user will not save, however if I update a user's account in the console the value will save. I'm using MySQL as my DB, could this be the issue?


54. Christoph Jan 07, 2010 at 02:38

@roberth: I had to add a user.roles_mask_will_change! to my migration code, to make changes hit the db. It was working correctly in the console. Not sure what the issue is. I'm running Rails 2.3.5.


55. Mark Dodwell Jan 20, 2010 at 17:23

Hmm.. personally, I dislike using a bitmask due to the issues raised above.

I've just released a simple gem for roles that simply stores roles as serialized YAML:
http://github.com/mkdynamic/miracle_roles


56. manolo blahnik shoes Jan 24, 2010 at 23:03

This is one of those "clever" solutions that will only cause


57. javon Jan 25, 2010 at 22:38

This is one of those "clever" solutions that will only cause


58. cheap adidas shoes Jan 31, 2010 at 19:16

Adidas Shoes Online Shop-Hot Selling Adidas Shoes & Cheap Adidas Shoes


59. wholesale electronic Feb 01, 2010 at 18:58

I can understand one-to-many associastion clearly but cannot many-to-many association

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player
Give Back to Open Source