Say you have two models with a one-to-many relationship (using belongs_to and has_many). For example, a manager has many employees.
# Manager
has_many :employees
# Employee
belongs_to :manager
Rails associations make it easy to find all the employees belonging to a manager (manager.employees
). And a rather simple find with conditions gets you the list of employees who belong to any manager, or even the orphaned employees with no manager.
# Employee
def self.find_managed
find(:all, :conditions => "manager_id IS NOT NULL")
end
def self.find_orphans
find(:all, :conditions => "manager_id IS NULL")
end
What if we need to find all the employees that don't work for a particular manager?
# Manager
has_many :employees do
def not
find(:conditions => ["employees.id != ?", proxy_owner.id])
end
end
That association extension lets us use manager.employees.not
to find the list of employees that don't work for that manager.
It's a bit harder to find manager with no employees at all. Still, we can do it without too much ugly SQL in our pretty Rails code. We want to find all managers for which there are no employees that belong to them.
# Manager
def self.find_without_employees
find(:all, :readonly => false,
:select => "managers.*",
:joins => "LEFT OUTER JOIN employees e ON managers.id = e.manager_id",
:conditions => "e.id IS NULL")
end
Then Manager.find_without_employees
returns all the managers without any employees. Time for a re-org!
It's nice that AR gives such good SQL hooks. I never really believed in ORM because it tends to be incomplete and clunky, however Rails is pretty good about letting you get around the incompleteness without much pain.
Sometimes I shudder to think of beginning programmers coming to Rails without understanding SQL or HTTP fundamentals and the horrific loads they must be generating on their SQL servers. But then I suppose that opinion is just like assembly programmers in the 70s who thought C was just too high level :)
Let me take the opportunity to advertise my Association Extensions plugin :-) It gives you methods for
Available from Rubyforge svn://rubyforge.org/var/svn/assocext/association_extensions/trunk
Well, interesting article, although I would like to ask/ test if it really is as magic as you describe. Two things came straight to my mind… and I would like to share them before I will go to bed.
When seeking emloyees without a manager, you are checking NULL, but how should this field be NULL? For sure you can create it this way, but I think in most cases employees will be created in association with managers… so this might be really seldom the case.
suppose you are new to ror and you created a manager and a lot of employees which belong to him. Now you delete the manager, but not the employees… so you have those employee which still seem to belong to the manager, but he is gone. the employees still would not have a NULL in the referencing manager_id field. How would you find them?
naxnam: on #2 you would just do the opposite of the last codesnippet I think, something like this maybe:
Gabe da Silveira: Fix a categorized atom/rss feed on your site? I want to add you to rubylicio.us :)
Very interseting. I didn't realize the below code was possible:
What other sorts of tricks can one do with blocks after such relational statements?
@naxnam: You can use the :dependent option on the manager:
When the manager is destroyed, all its employees' manager_id fields will be set to nil/NULL.
The standard way of doing thing in Rails is to use the business logic in the models to manage foreign key relationships (the above being an example of how to do that). If that isn't sufficient you can always fall back to foreign key constraints in the database. But while that kind of constraint may seem safer, I think it actually leads to sloppy thinking about the business logic and potential messes because you don't really understand the relationships.
@james h: Those blocks are called association extensions. They are documented in the ActiveRecord API docs, and I have several examples of using them in other articles on the blog. They let you create new methods in the association, and are a powerful and convenient way to give associations richer behavior.
Thanks Josh, very helpful tip.
Josh, Thanks for the post. I've already been using Association Extensions but was unware that proxy_owner mapped to the Association Proxy.
I've been trying to figure out how to do this for a while and have been obviously using the wrong search terms. This will allow me to do same more elegantly.
On a side issue is there any way you can retrieve the scope of the proxy_owner such as the joins, conditions etc?
Thanks, John
@John Ward: the
proxy_reflection
accessor of the association proxy returns the association's reflection object. You can use that to find all the options on the association just as joins and conditions. And for completeness,proxy_target
returns the singular object (for has_one and belongs_to) or collection that is returned by the association.Thanks for your reply.
John
I was wondering if the following declaration:
shouldn't be
since in your example you intended it to be a class method. If not, is there a fancy way to declare class methods in rails?
Thanks! brooks
Brooks, good catch. Fixed.
Josh,
quick note, the two Employee examples need to have self dot in front of them also, since they should be class methods too.
great blog btw.
Is there an elegant way to get (in one fell swoop?) all the "this employe works for, this employee doesn't work for" info for one specified manager (or all managers at once)? I tried LEFT JOIN-ing (and :include), but when I add in the :conditions to specify a manager, that blows away all the "nil" results from the JEFT JOIN.
Or, for an analogous example, I've got :employees and :trainingsessions, and :trainingscores as a hasmany :though join table. I'd like a way (without repetitively hammering the MYSQL db) to display a grid of :employees and :trainingsessions, so that I can (with a glance at a rhtml "View") see if a specific employee has taken a specifc training_session.
I've got a solution that brute force iterates through all :employees and all :training_sessions, but there must be a more elegant solution....
Or is there?