DZone Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world
The Beauty Of A Nested ':include' Option
So you've got a lot of stuff, spread over a lot of tables, and now you want to scoop it all up and display it all in the same view - oh, and you don't want ActiveRecord to perform a bazillion subsidiary SQL lookups along the way. The answer - a nested :include clause.
The example:
A model...
class Organizer < ActiveRecord::Base has_many :events end class Event < ActiveRecord::Base belongs_to :organizer has_many :bookings end class Booking < ActiveRecord::Base has_many :sessions belongs_to :event end class Session < ActiveRecord::Base belongs_to :booking end
... and in my controller i want to...
Booking.find_all_by_date(Date.today)
Now this all works fine... up until a certain line in my '_booking' view partial:
<%= h(booking.event.organizer.name) %>
...and a little further along, when i do a:
<%= render :partial => 'session', :collection => booking.sessions %>
Both of which work fine too. Unfortunately, my view has to execute the 'booking' partial a total of three times, and each time it does so, ActiveRecord performs three additional SQL queries - one for the events table lookup, one for the organizers table lookup and one for the sessions table lookup. Along with the original 'bookings' query, that comes to a total of 10 SQL queries to display the details of three bookings. Hardly very efficient. :include to the rescue (maybe):
Booking.find_all_by_date(Date.today, :include => [:event, :sessions])
Sure enough, this pre-loads (or eager loads) the events and sessions table info into the model, thus allowing me to use a line like 'booking.event.title', or 'booking.sessions[0].start_time' without firing off another SQL query. So now my query count drops to just four - one to fetch the combined booking/event/sessions data and the other three to fetch each organizer. All good. So, then I tried this:
Booking.find_all_by_date(Date.today, :include => [:event, :organizer, :sessions])
Wrong!! I got an error because ':organizer' is not a valid association of the Booking model (ie, there's no 'has_one', 'has_many' or 'belongs_to' referring to the Organizer model from the Booking model) I flailed around on the internet for a few hours searching for (and trying out) a number of solutions - mostly revolving around 'find_by_sql'. Though a couple of these solutions successfully fetched the data, they didn't then populate the model objects correctly. Instead, 'find_by_sql' seems to add new accessors to my Booking class - this would allow me to use a line like 'booking.organizer.name', but a call to the real object (booking.event.organizer.name) would still generate a SQL query. Not acceptable. The most interesting possibility I found was using a :through clause. This allows you to refer to one association 'through' another like this:
class Booking < ActiveRecord::Base belongs_to :event belongs_to :organizer, :through => :event end
Unfortunately, the :through clause only works when you're passing :through a 'has_many' association. It doesn't work for 'has_one' or 'belongs_to' (didn't try 'has_and_belongs_to'). So, finally, in a fit of mingled frustration/inspiration (and knowing the nested nature of some of the rails code) i tried this:
Booking.find_all_by_date(Date.today, :include => [{:event, :organizer}, :sessions ])
My reasoning: well, since :organizer is a valid association of the Event model, I thought that if I packaged those two together in a kind of sub-include, it might actually do a nested call to whichever magic rails method constructs the SQL queries.
I've since found out that the correct syntax for this clause should be ':include => [{:event => :organizer}, :sessions ]'. Apparantly when you throw an even number of comma-separated items into a hash literal, ruby groups them into :key => :value pairs for you - what an amazing language it is! :-) So the corrected code snipped should be:
Booking.find_all_by_date(Date.today, :include => [{:event => :organizer}, :sessions ])
And Eureka!! A single (hefty) SQL query fetched all relevant data, whereupon ActiveRecord dutifully (and apparently recursively) populated all the model objects that my view template needed. I'm happy. For reference: I'm using Ruby 1.8.4, Rails 1.1 and the SQLite3 database.





Comments
Aashish Kiran replied on Fri, 2012/11/23 - 5:59am
Hi,
Thanks for spending time in writting this post. It is a good post. It added a joy in finding out hidden beauty of rails.
Thanks for all the help through this post.
Keep posting !!
Snippets Manager replied on Tue, 2012/01/03 - 6:49pm
Snippets Manager replied on Tue, 2012/01/03 - 6:49pm
Snippets Manager replied on Tue, 2009/02/24 - 9:50am
Snippets Manager replied on Fri, 2008/04/04 - 4:04pm
Snippets Manager replied on Sat, 2007/06/30 - 10:02pm
Snippets Manager replied on Wed, 2007/06/20 - 1:11pm
Snippets Manager replied on Tue, 2007/06/05 - 12:38pm
Hassal Fathi replied on Fri, 2007/01/19 - 1:03am
@organizer=Organizer.find(:first, :include => [{:events => {:bookings => :sessions}}])So you can indeed eagerly load through more than one level. You won't be able to access@organizer.bookingsdirectly though, you'll have to go through :@organizer.events.first.bookingsBye, EricSnippets Manager replied on Mon, 2012/05/07 - 2:24pm
Snippets Manager replied on Mon, 2012/05/07 - 2:24pm
SELECT work_units.`id` AS t0_r0, work_units.`part_id` AS t0_r1, work_units.`operation_id` AS t0_r2, work_units.`estimated_hours` AS t0_r3, work_units.`scheduled_hours` AS t0_r4, work_units.`status` AS t0_r5, work_units.`quantity_completed` AS t0_r6, parts.`id` AS t1_r0, parts.`job_id` AS t1_r1, parts.`title` AS t1_r2, parts.`width` AS t1_r3, parts.`height` AS t1_r4, parts.`quantity` AS t1_r5, parts.`filename` AS t1_r6, jobs.`id` AS t2_r0, jobs.`job_code` AS t2_r1, jobs.`due_by` AS t2_r2, jobs.`production_objective` AS t2_r3, jobs.`title` AS t2_r4, jobs.`customer_id` AS t2_r5, jobs.`created_on` AS t2_r6, jobs.`customer_po` AS t2_r7, jobs.`closed_at` AS t2_r8, jobs.`invoice_number` AS t2_r9, jobs.`invoice_amount` AS t2_r10, jobs.`hours_cache` AS t2_r11, jobs.`csr_id` AS t2_r12, jobs.`sales_id` AS t2_r13, jobs.`carrier_id` AS t2_r14, jobs.`delivery_instructions` AS t2_r15, jobs.`installation` AS t2_r16, jobs.`installation_instructions` AS t2_r17, operations.`id` AS t3_r0, operations.`schedulable` AS t3_r1, operations.`code` AS t3_r2, operations.`name` AS t3_r3, operations.`department_id` AS t3_r4, operations.`hourly_rate` AS t3_r5, operations.`markup` AS t3_r6, operations.`billable` AS t3_r7 FROM work_units LEFT OUTER JOIN parts ON parts.id = work_units.part_id LEFT OUTER JOIN jobs ON jobs.id = parts.job_id LEFT OUTER JOIN operations ON operations.id = work_units.operation_id ORDER BY parts.titlewe can write@work_units = WorkUnit.find(:all, :order => 'parts.title', :include => [{:part => :job}, :operation])According to the rdocs, belongs_to and has_many accept an :include option. I'm wondering if this will do the eager loading on every query rather than having to explicitly state it in the find()?Snippets Manager replied on Sun, 2006/05/28 - 1:58am
Joe Martinez replied on Tue, 2006/05/23 - 10:18pm
Snippets Manager replied on Mon, 2012/05/07 - 2:23pm