homeASCIIcasts

189: Embedded Association 

(view original Railscast)

Other translations: Cn It

In the previous episode we created a role-based authentication system. On the application’s sign-up page a series of checkboxes allowed a user to assign themselves to one or more roles.

The signup page showing the roles checkboxes.

The application has a Role model in which the roles are defined and a many-to-many relationship between the Role and User via an Assignment model. If we look in the database we’ll see the three existing roles.

  >> Role.all
  +----+-----------+-------------------------+-------------------------+
  | id | name      | created_at              | updated_at              |
  +----+-----------+-------------------------+-------------------------+
  | 1  | Admin     | 2009-11-16 21:22:59 UTC | 2009-11-16 21:22:59 UTC |
  | 2  | Moderator | 2009-11-16 21:23:06 UTC | 2009-11-16 21:23:06 UTC |
  | 3  | Author    | 2009-11-16 21:23:16 UTC | 2009-11-16 21:23:16 UTC |
  +----+-----------+-------------------------+-------------------------+
  3 rows in set
  

The main problem with this set up is that there is a tight coupling between the roles in the database and the code in the authorization_rules file that defines the permissions for that role.

  role :author do
    includes :guest
    has_permission_on :articles, :to => [:new, :create]
    has_permission_on :articles, :to => [:edit, :update] do
      if_attribute :user => is { user }
    end
  end
  

With the role and permissions defined this way we can’t make changes to the roles table without also needing to change the Ruby code that defines each role and so the benefits of storing the roles in the database are lost. This episode will show you how to remove this coupling by defining the roles only in the code and not in a database table.

As we’ll no longer have a Role model the first change we’ll make is to the User model where we’ll remove the associations with assignments and roles by deleting the following two lines.

  has_many :assignments
  has_many :roles, :through => :assignments
  

Having done this we’ll have to create the roles somewhere else. As the roles are related to users we’ll define them as a constant in the User model.

  class User < ActiveRecord::Base
    acts_as_authentic
    has_many :articles
    has_many :comments

    ROLES = %w[admin moderator author]

    def role_symbols
      roles.map do |role|
        role.name.underscore.to_sym
      end
    end
  end
  

Now we only have to modify the code when we need to alter the roles. But the question remains: how to we associate a user with their roles? As we no longer have a roles table we’re going to have to embed this association within the User model somehow.

Below we’ll show you two ways of doing this, dependent on the type of association you’re working with.

Embedding a One-to-Many Relationship

Currently our application has a many-to-many relationship between users and roles. We’ll change this to a one-to-many relationship so that a user can only be a member of one role. The role will have to be stored in the users table but as we’re just storing a single value the role can be stored in a string field. We’ll generate the role field with the following migration.

  script/generate migration add_role_to_users role:string
  

Then we’ll migrate the database to add the new field to the table.

  rake db:migrate
  

The next step is to modify the sign-up form to replace the roles checkboxes with a select menu. To do this we’ll replace this code in /app/views/users/new.html.erb.

  <%= form.label :roles %>
  <% for role in Role.all %>
    <%= check_box_tag "user[role_ids][]", role.id, @user.roles.include?(role) %>
    <%=h role.name %><br />
  <% end %>
  

with this:

  <%= form.label :role %>
  <%= form.collection_select :role, User::ROLES, :to_s, :humanize %>
  

We’re using collection_select to generate the select menu. This is usually used with a model, but it works just as well in this scenario. When used with a model we pass it a collection of model objects and the properties for the model that should be used to set the value and text for each option. Instead of that we pass it the array of role names that we defined in the User model. We set the value for each option by calling :to_s on each role and the display text by calling :humanize on each role to prettify it for display. The result of this is a select menu that allows users to select a single role.

The checkboxes have now been replaced by a select menu.

As we’re using declarative authorization in our application we’ll also need to make a change to the role_symbols method in the User model, changing so that it returns the user’s role converted to a symbol.

  def role_symbols
    [role.to_sym]
  end
  

Now when we create a new user their role is stored in the role field in the users table.

  >> User.last.role
  => "moderator"
  

Embedding a Many-to-Many Relationship

So we’ve successfully embedded a one-to-many relationship into a single model but what if we want to keep the ability for users to assign themselves to many roles without having to restore the Role model? This is a little more difficult to achieve as we have to squeeze multiple values into a single column in the users table.

One solution would be to create a text column called roles in our users table and use Rails’ serialize method to store the user’s roles in that column. This will call to_yaml on the roles before storing the field in the database. This approach works but it makes it difficult to do things such as finding all of the users in a given role so we’ll try a different approach.

Instead of serializing the roles we’re going to use a bitmask. This way we can store multiple values in a single integer column and still have the ability to retrieve users by their role.

The first thing we’ll do is add a new integer column called roles_mask to the users table to store the bitmask value.

  script/generate migration add_roles_mask_to_users roles_mask:integer
  

Then run the migration

  rake db:migrate
  

In the User model we’ll need to handle the roles_mask by writing getter and setter methods so that we can convert easily between the bitmask and an array of roles.

  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
  

The setter method takes an array of roles and converts it into a bitmask integer which is assigned to the roles_mask attribute. The getter loops through each role and returns an array of the roles whose bit is set in the mask. There are plugins available to make bitmasks easier to work with but as we only have one field and have only needed to write a few lines of code to implement this we won’t use one.

Again we’ll need to change the role_symbols method to work with our altered roles. To do this we’ll take the array of roles and convert each one to a symbol.

  def role_symbols
    roles.map(&:to_sym)
  end
  

The final change we need to make is to alter the view again to replace the select menu with checkboxes so that a user can pick a number of roles when they sign up.

  <%= form.label :roles %>
  <% for role in User::ROLES %>
    <%= check_box_tag "user[roles][]", role, @user.roles.include?(role) %>
    <%= h role.humanize %>
  <% end %>
  <%= hidden_field_tag "user[roles][]"%>
  

The technique used here to create checkboxes for a many-to-many relationship is similar to the one shown back in episode 17 [watch, read]. The empty square brackets in user[roles][] mean that the values of the checked checkboxes will be passed as an array to the User model where the setter method we wrote will convert them to a bitmask value. The last parameter passed to check_box_tag will check the checkbox if the user is a member of that role. The hidden field ensures that an empty array is passed if no checkboxes are checked.

When we go to the sign-up form now the checkboxes have returned. We’ll sign up a new user and assign them to two roles.

Signing up a new user and assigning their roles.

If we look at the newly-added user in the console we can see their roles and the value assigned to their roles_mask.

  >> User.last.roles
  => ["admin", "author"]
  >> User.last.roles_mask
  => 5
  

Each role has a value twice that of the role next to it, with the lowest-value role having a value of 1. So the value of 5 for our user is calculated from (1*1) + (2*0) + (4*1).

Having stored our roles in a single column how do we find all of the users in a given role? Let’s say we want to find all of the users with moderator privileges. We can do this by adding a named scope to our User model.

  named_scope :with_role, lambda { |role| {:conditions => "roles_mask & #{2**ROLES.index(role.to_s)} > 0 "} }
  

This named scope takes a role as an argument and performs a bitwise operation on it to determine whether a user belongs to that role. We can test this in the console by finding all of the users in the admin role.

  >> User.with_role("admin")
  +----+----------+-------------+-------------+-------------+-------------+------------+
  | id | username | email       | crypted_... | password... | persiste... | roles_mask |
  +----+----------+-------------+-------------+-------------+-------------+------------+
  | 6  | paul     | paul@tes... | cffada11... | FDGoNtM1... | 35a7d8c8... | 5          |
  +----+----------+-------------+-------------+-------------+-------------+------------+
  1 row in set
  

This returns only the most recent user as this is the only user we’ve added since we created the roles_mask field.

If you nned to add extra roles to the application later there’s a potential trap you need to tale care with. As the bitmask is based on the position of the role in the ROLES array you can only add new roles to the end of this array. If new roles are added at the start or in the middle of the array then any existing users may have their roles changed.

  ROLES = %w[admin moderator author editor]
  

Adding an extra role to the list.

That’s it for this episode. Bitmasking is a powerful technique for embedding a many-to-many relationship within a single integer attribute in a model. That said, it’s only worth applying this method if you have a list of records that don’t belong in a database table. Our list of roles is so tightly bound to the code that it’s pointless storing them outside the code itself as we can’t change a role without having to make changes to the code to define that role’s permissions. If you can see a situation where you could add or alter one of these records without needing to make changes to the code then the traditional many-to-many relationship in the database is still the best approach.