#189 Embedded Association
Nov 23, 2009 | 14 minutes | Active Record, Views
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:
- source code
- mp4
- m4v
- webm
- ogv
Awesome been looking forward to this one!
Great one, thanks a lot!
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 ^^
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.
Thanks a lot Ryan!
I've been using serialize lately and searching for matching records was such a pain :)
I've got UG...
Just kidding, thanks a lot Ryan! Keep up the great work.
Thanks
Ben
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.
nice screen cast and nice to see no spams (YET!!!)
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!
Make that "role like '%admin%'".
Ryan, you're my role model ;-}
@Ryan, thanks a lot for introducing me the bitmask concept. Definitely I will use it in my project.
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
I'd be afraid of this making my relationships too complex. I'll give it a try!
Have a look at the FlagShihTzu plugin that handles all these things for you:
http://github.com/xing/flag_shih_tzu
Wonderful! I was actually wondering if this was posible. Thanks a lot.
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
@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.
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
Here's a neat trick that works with your code to add magic methods like "user.admin?":
http://gist.github.com/241546
I just love bitmasks
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.
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!
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!
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.
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...
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!!
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? }
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.
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]
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!
@gene: The tool for irb is called 'hirb' and could be found here:
http://github.com/cldwalker/hirb/tree/master
Thank you very much, that seems really useful
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'
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! :)
Hello,
what kind of Plugin is it, to wrtie "Role.all" and you get lovely formattet answer?
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?
How can i make this
<%= f.collection_select :role, User::ROLES, :to_s, :titleize %>
In formtastic? The part for :humanize or :titleize specially.
Thanks!
Hi
How you get formatted Role.all ?
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?)
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.
"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 :-).
also check out bitmask_attributes: http://github.com/amiel/bitmask_attributes
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"} }
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?
@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.
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
Hello Ryan, Great Work =)
For the ones asking:
To format the console use the Hirb gem.
http://github.com/cldwalker/hirb
@Sigi: "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."
The coupling of concern came from declarative authorization, where the roles which are in the database are now embedded in the Ability model. Install the app somewhere else and that will break unless the db contents are duplicated. By moving the roles into the user model, the code is not referring to the contents of the database and the coupling is removed.
The roles of a given user (and what is stored) are *always* going to be related to the list of available roles. This is not coupling. This will be the case no matter how you do authorization.
This is of minor use, but I found myself needing groups of roles. Here is my User model which includes several suggestions (@Marc-Andre Lafortune, @Matt Werner, and @琳琳的小狗 above.)
http://pastie.org/883113
The basic idea is to prefix role names with groups (employee_admin, employee_manager), add a hash of masks, and an associated function.
Thanks Ryan for CanCan and the weekly work on the RailsCast.
Ryan.
I am testing this using a constant, the first part of the video with just some strings in the model.
I have one question, once a role is updated, how can i display the role in the view?
I tried <%=h @user.role %>
But no luck, basically I just want to display something like role: admin.
how do i go around this?
Very nice solution, but is there a specific reason to not use Boolean tables within the user model for each role ? (user.amin, user.moderator etc.) It seems to be easier this way to delete roles and all the corresponding data through migrations.
Scoping is slightly different in rails 3.2 as shown below:
Essentially,
lambda
is replaced by->
andnamed_scope
byscope
in rails 3.2I think this is wrong, it has changed but it gives error using your syntax.
Curious if this is still a valid way of using the Enum in Rails 4.1? Really want to have something defined on a model without creating a second model -- it's static a static list of locations --this seems like the best way of dealing with it.
This is the correct syntax for scoping in rails 4+
@Stijn, thank you - this bit me when migrating from Rails 3.x to 4.x