Peter's Blog

Redefining the Impossible

has_and_belongs_to_many


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.


Filed under: noob rails

Tim Says:

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?

Peter Says:

It's a while since I dealt with this but I'm fairly confident to say that it doesn't matter, the relationship is fairly symetrical and rails will look for either a roles_users table or a users_roles table, whichever exists.

I will emphasise that this is an assumption but rails is built on ruby which has a philosophy of 'least surprise' and it would surprise me if I was wrong smile

Peter

Dennis Says:

You should name it in alphabetical order -- r < u so roles_users is the default that rails will look for.

Have Your Say

I welcome constructive comments or questions but I reserve the right to delete any comments that displease me.

Who are you?

(Optional) If you enter an email address here I might email you back. Your email address will not be sold to spammers or shown anywhere

What do you have to say?