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.