Ooh Err, has_and_belongs_to_many, how_can_a_keyword_be_so_long?
Anyway, I've just used it for the first time to implement a UI in rails for user role permissions and here are some notes so that I might be able to use it again without excessive code trawling.
has_and_belongs_to_many is used to define many to many relationships. In the system I am implementing I have many Users and many Roles for them to perform. A User can be assigned to many Roles and a Role may be assigned to many Users. This is a classic many to many relationship and is implemented by having a table that contains a list of mappings of user id to role id, one mapping for each assignment of a user to a role.
This is (essentially) the migration that is used to create the user that is created by acts_as_authenticated but here it is stripped to the meaty bits:
1 class CreateUsers < ActiveRecord::Migration 2 def self.up 3 create_table "users", :force => true do |t| 4 t.column :login, :string 5 end 6 end 7 8 def self.down 9 drop_table "users" 10 end 11 end
This is the migration that the role_requirement plugin uses to create the roles tables:
1 class CreateRoles < ActiveRecord::Migration 2 def self.up 3 create_table "roles" do |t| 4 t.column :name, :string 5 end 6 7 # generate the join table 8 create_table "roles_users", :id => false do |t| 9 t.column "role_id", :integer 10 t.column "user_id", :integer 11 end 12 add_index "roles_users", "role_id" 13 add_index "roles_users", "user_id" 14 end 15 16 def self.down 17 drop_table "roles" 18 drop_table "roles_users" 19 end 20 end
In summary, there are three tables in the database:
- A 'users' table with a field called 'login' that gives the user login name
- A 'roles' table with a field called 'name' that gives the name of the role
- A table called 'roles_users' that holds each assignment of a role to a user.
Models need to be created for the User and Role table but not the roles_users table as that one is handled automagicaly by rails.
Here are the meaty bits of the model for the User table:
class User < ActiveRecord::Base has_and_belongs_to_many :roles validates_length_of :login, :within => 1..40 validates_uniqueness_of :login, :case_sensitive => false end
And the model for the Role table:
class Role < ActiveRecord::Base has_and_belongs_to_many :users validates_presence_of :name validates_uniqueness_of :name, :case_sensitive => false end
Both these have the magical has_and_belongs_to_many declaration to invoke the many-to-many goodness. They also perform some validation on what the user enters such as making sure they are giving their users login names.
Now for some code for a partial that can be used in a view to edit or create user records. This is used to generate a list of check boxes, one for each role. The check box will be checked according to whether or not the user has been assigned that role:
1 <% form_for :user, :url => { :action => action, :id => @user} do |f| %> 2 3 <p>Enter the login name for the new user:</p> 4 5 <div style="margin-left: 50px; margin-bottom: 50px"> 6 <%= f.text_field :login %> 7 </div> 8 9 <p>Select the Roles that this user can perform</p> 10 11 <div style="margin-left: 50px"> 12 <table> 13 <% for oRole in Role.find(:all, :order => :name) %> 14 <tr> 15 <td> 16 <%= check_box_tag "user[role_ids][]", oRole.id, @user.roles.include?(oRole) %> 17 </td> 18 <td> 19 <%= oRole.name %> 20 </td> 21 </tr> 22 <% end %> 23 </table> 24 </div> 25 26 <%= submit_tag submit_tag %> 27 <%= submit_tag "Cancel" %> 28 <% end %>
Here we iterate through all the Roles in the role table, sorted into name order. For each role we generate a check box tag. The check_box_tag line is tricky but can be broken down as:
- "user[role_ids][]"
- this is the name for the check box tag field. Naming the check box like this ensures that when the form is posted back to the server, the values for each tag will be placed correctly in the params array
- oRole.id
- the id of the Role record for this checkbox. It is these ids that are stored in the roles_users table to map a user id to a role id.
- @user.roles.include?(oRole)
- from the current user record, search the list of roles mapped to that user and see if the list already contains a particular role. If it does then the checkbox will be checked when the form is displayed.
When the form is submitted we need to ensure that the roles mapped to a user are updated correctly. This is simple:
1 def update 2 strLogin = params[:user][:login] 3 4 oUser = User.find( params[:id]) 5 oUser.login = strLogin 6 oUser.role_ids = params[:user][:role_ids] 7 oUser.save! 8 flash[:notice] = "Updated User '#{strLogin}'" 9 10 redirect_to :action => :list 11 end
This is the method in the user controller that updates an existing record. The juicy bit is the line
oUser.role_ids = params[:user][:role_ids]
which is ALL it takes to update all the role assignments! This saves a lot of work such as adding new role assignments and removing extraneous ones.
Another little thing to watch out for: when deleting a role or a user do NOT use the delete method:
User.delete( params[:id]) ## WRONG
delete apparently just sends the raw sql to the database engine to get it to delete the object. Instead you should call destroy:
User.destroy( params[:id]) # Delete user and database objects associated with him/her
destroy will invoke the rails Active Record magic that will cause the roles_users table to be updated, removing any entires for roles or users that are being deleted. Calling delete will not do this and will leave stray records in the database.


How does Rails know that it should use the "roles_users" table as opposed to one named "users_roles".
Basically, when you create that intermediary table, how did you know which name to give it?