Consider the following problem domain. We have users, projects as well as roles and rights. Often, users are assigned roles and roles are assigned rights in straightforward binary associations:

[rights] –< [roles] –< [users]

However, what if we introduce the notion that users can be assigned to various projects and be given different roles from one project to the next. So a user may be a manager of one project, or just a contributor on another. Also, what if the rights assigned to roles can themselves vary from one project to the next. So the rights given to a manager on one project may differ from the rights given to a manager on another project.

Given just these slight adjustments not atypical in real world applications, we now have a fairly complex set of relationships that include “bridge” or “association” tables that connect three tables, not just two:

[roles] –< [project_right_roles] >– [rights] | / | | ^ | [users] –< [project_role_users] >– [projects]

When it comes to coding this in a Rails application, there are plenty of examples of binary associations, but in my searching, I have found no examples of how to implement ternary relationships, a void I hope to now fill. The key will be to make the association tables into full-fledged models and use them in has_many :through declarations to specify the relationships.

Let’s create a RESTful Rails application based on our data model. For the sake of brevity, I will just show the commands for doing this, but not their output:

rails hat_example cd hat_example ./script/generate scaffold_resource user username:string first_name:string last_name:string email_address:string ./script/generate scaffold_resource project name:string description:text ./script/generate scaffold_resource role name:string position:integer ./script/generate scaffold_resource right name:string postion:integer ./script/generate model project_role_user project_id:integer user_id:integer role_id:integer ./script/generate model project_right_role project_id:integer right_id:integer role_id:integer mysqladmin -u root create hat_example_development rake db:migrate

For about 30 seconds of typing, Rails has done a lot of work for us: it created the database structure, the classes that represent that database structure, basic CRUD interfaces to those classes and the controllers to tie it all together.

Each of our main tables was created as a resource in Rails using the scaffold_resource generator. And, because we know that our association tables will have to be full blown models, but won’t be directly managed RESTfully themselves, we just generated models for them. For the role and right resources, I added the position field to give us the option later on of arbitrarily ordering individual resources on the screen. By using the name “position” and adding the “acts_as_list” declaration to the corresponding models, we will be able to take advantage of some built-in Rails magic to easily implement this.

Now we just need to modify each model class file in the app/models directory to specify the relationships among the tables and finish the basis for our application. To define the ternary relationships, has_many :through declarations will be made to let ActiveRecord know how to navigate among the tables in our ternary relationships. Again, for the sake of brevity, I am going to present what each file will look like within the same space below:

# in app/models/user.rb class User < ActiveRecord::Base has_many :projects, :through => :project_role_users has_many :roles, :through => :project_role_users, :order => :position validates_presence_of :username validates_uniqueness_of :username def inverted_full_name last_name + “, ” + first_name end end # in app/models/project.rb class Project < ActiveRecord::Base has_many :rights, :through => :project_right_roles, :order => :position has_many :roles, :through => :project_right_roles, :order => :position has_many :roles, :through => :project_role_users, :order => :position has_many :users, :through => :project_role_users validates_presence_of :name validates_uniqueness_of :name end # in app/models/role.rb class Role < ActiveRecord::Base has_many :users, :through => :project_role_users has_many :projects, :through => :project_role_users has_many :projects, :through => :project_right_roles has_many :rights, :through => :project_right_roles, :order => :position acts_as_list validates_presence_of :name validates_uniqueness_of :name end # in app/models/right.rb class Right < ActiveRecord::Base has_many :projects, :through => :project_right_roles has_many :roles, :through => :project_right_roles, :order => :position acts_as_list validates_presence_of :name validates_uniqueness_of :name end # in app/models/project_right_role.rb class ProjectRightRole < ActiveRecord::Base belongs_to :project belongs_to :right belongs_to :role end # in app/models/project_role_user.rb class ProjectRoleUser < ActiveRecord::Base belongs_to :project belongs_to :role belongs_to :user end

As you can see, I added some basic validations that seemed generally reasonable. I think the thing that was most likely to trip me up in this process was knowing what naming conventions to use on the :through declarations. As I am still quite new to Rails, and the generators usually set up the appropriate conventions for me, I often don’t have to think through what conventions to use myself.

This is obviously just a bare skeleton of the data model to get us started. Let’s show a quick example of how to use these relationships in a view. Lets say that when I display a user, I want to show which projects they belong to, and which roles they have on those projects. in app/controllers/users_controller.rb, my show method might look like this:

def show @user = User.find(params[:id]) @user_projects = ProjectRoleUser.find(:all, :conditions => [‘user_id = ? ‘, @user.id], :include => [:project, :role]) respond_to do |format| format.html # show.rhtml format.xml { render :xml => @user.to_xml } end end

This is the standard show method produced by the scaffold_resource generator, except that I’ve added the @user_projects variable to provide the needed information to the view. A few things to notice that may not be obvious: I queried against the association model/table to limit the results to the indicated user while still allowing me to quickly get to both the project and role information for each result. The :include declaration further supports this through “eager loading”–it tells ActiveRecord to fetch the corresponding project and role records for each result. Otherwise, when I try to access this information later, it will fire off new queries to retrieve that data.

So, in the corresponding view in app/views/users/show.rhtml, I can display the information like so:

<h3> Projects I Own </h3> <% if @user_projects.empty? %> You do not yet own any projects. <% else %> <% for user_project in @user_projects %> <ul id=”projects_owned”> <% if user_project.role.name == ‘owner’ %> <li> <%= link_to user_project.project.name , project_path(user_project.project) %> </li> <% end %> </ul> <% end %> <% end %>

Related Post