Never been to DZone Snippets before?

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 (See related posts)

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 on this post

timmorgan posts on May 23, 2006 at 17:11
Thank you for posting this. This is a handy snippet I will file away in my brain for later use...
jrm posts on May 23, 2006 at 22:18
This is a great post!
itchybeard posts on May 28, 2006 at 02:32
I would like to thank you too for this, with your post I might manage to break the habit of littering my models with find_by_sql :)
willcodeforfoo posts on Sep 08, 2006 at 14:32
That's pretty slick, nice find! Thank goodness for Rails so instead of writing:

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.title


we 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()?
willcodeforfoo posts on Sep 08, 2006 at 14:36
Oh, and it doesn't seem to be working for associations deeper than one level, does it?
Ricou posts on Jan 19, 2007 at 00:11
Thanks a lot for this tip, thairond!
I divided by 1000 the number of my SQL requests :)

@willcodeforfoo, I just discovered that you can type this :
@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.bookings

directly though, you'll have to go through :
@organizer.events.first.bookings


Bye,
Eric
tomsmyth posts on Jun 05, 2007 at 12:41
Note that the nesting can go several levels deep. This worked for me:

:include => [:sometable, {:torrent_entry => {:entry => [:category, :tags, {:attrib_values => :attrib_key}]}}])
Vishwa posts on Jun 20, 2007 at 13:23
You might want to consider using :include option in the find to select only the columns of interest from
the :include table set.
e.g.,
WorkUnit.find(:all, :select => "work_units.id, parts.job_id, operations.name"
:order => 'parts.title', :include => [{:part => :job}, :operation])

Note: this does require you to install the select_with_include gem
malbert posts on Jun 30, 2007 at 22:04
Thanks a bunch! I spent the whole afternoon looking for the elegant solution, and this was it.
kwerle posts on Apr 04, 2008 at 16:10
Very useful. Thanks much.

You need to create an account or log in to post comments to this site.


Click here to browse all 4863 code snippets

Related Posts