complex associations in rails activerecord
During playing with CanCan from ryanb show in last railscasts I did planned some models and find me in quite strange situation – I knew the connection, but I could not easily get the result.
My models look something like:
class User < ActiveRecord::Base has_many :assigments has_many :groups, :through => :assigments end class Assigment < ActiveRecord::Base belongs_to :user belongs_to :group end class Group < ActiveRecord::Base has_many :assigments has_many :users, :through => :assigments has_many :responsibilities has_many :roles, :through => :responsibilities end class Responsibility < ActiveRecord::Base belongs_to :role belongs_to :group end class Role < ActiveRecord::Base has_many :responsibilities has_many :groups, :through => :responsibilities end
So my first try was to get roles through groups manually:
User.first.groups.map{|g| g.roles }.flatten.map{|r| r.name.to_sym }.uniq
But this solution is waste of resources, for system with a lot of possible roles it might be very inefficient, it takes first all groups of user and then ittereting through them gets all it’s roles. it will generate a lot of database queries.
So after some searching I got following code:
User.first.roles.map{|r|r.name.to_sym}.uniq
To make it working I used one quite nice feature of Active record – :finder_sql – so in user.rb I have added following line:
has_many :roles, :finder_sql => 'SELECT r.* FROM users u LEFT JOIN assigments a ON u.id=a.user_id LEFT JOIN responsibilities res ON a.group_id=res.group_id LEFT JOIN roles r ON res.role_id=r.id'
This makes it possible to get all the roles for a user just in one call, everything calculated on database side. Unfortunately there is one down side of this – the SQL code might be not portable to other databases, so use it only when You are sure You will stick to one database.
There is also other way around, it allows to get the same data in two sql calls, without using SQL queries, just on pure ActiveRecord usage:
Responsibility.find_all_by_group_id( u.assigments(:select=>'assigments.group_id').map{|a|a.group_id}, :select=>'roles.name', :joins=>:role ).map{|r| r.name.to_sym }.uniq
There are many ways to archive the same goal, knowing them is only an part – knowing how they work, makes us aware how to chose the path.
on 2009-01-31 13:15 added:
Another way going out of the Group class:
Group.find( :all, :select=>'roles.name', :joins=>[:users,:roles], :conditions=>{:users=>{:id=>1}} ).map{|g|g.name.to_sym}.uniq
Did You liked this post or maybe not, vote on it at dzone.