Rails tip: Batch load associations
- rails,
- ruby
The Tip
Do you have a collection of Records that can be displayed in multiple different views? If your application is
anything more than minuscule, you probably do - and each of those views likely displays a distinct set of the Records’
associations. Rails provides the handy includes
method for you to eager-load these associations, but in this
case it would be wasteful to load the superset of associations for both views (scale this example up to a dozen views,
and “wasteful” becomes “untenable”).
Handily, Rails provides preload_associations
to fix us up:
class Record < ActiveRecord::Base has_many :details has_one :owner belongs_to :record_set def self.set_one records = get_some_awesome_set_of_records preload_associations records, :details => :even_more_details end def self.set_two records = get_some_awesome_set_of_records preload_associations records, [ { :owner => :address }, :record_set ] end end
Doing this pre-loading of associations in the model might not be the best place, however, so thanks to a tip from
Arne Hartherz of makandra, we can make preload_associations
public, and let our actions, or presenters, or whatever has the knowledge of the necessary associations do the pre-loading.
The Story
Searching for a way to do this (back in January) turned out to be surprisingly difficult - mainly because I was looking for a way to “batch load associations” in Rails. A myriad of combinations of that phrase didn’t turn up much at all - the most relevant being this blog post on “bulk-loading associations”.
Armed with a relative surety that nothing like this existed, I made a few quick and dirty methods to do just this.
Without knowledge of ActiveRecord::Reflection
and the reflections
method, these methods were pretty limited (but also
relatively simple), e.g.:
def preload_belongs_to_association_for(records, association_name, options = {}) records_with_unloaded_associations = records.reject do |record| record.nil? || record.send("loaded_#{association_name}?") end return unless records_with_unloaded_associations.present? options[:primary_key] ||= :id options[:foreign_key] ||= "#{association_name}_id".to_sym records_by_id = records_with_unloaded_associations.group_by &(options[:foreign_key]) finder = association_name.to_s.classify.constantize. where(options[:primary_key] => records_by_id.keys.uniq) finder = finder.includes(options[:includes]) if options[:includes] assocated_records_by_primary_key = finder.to_a.index_by &(options[:primary_key]) records_with_unloaded_associations.each do |record| record.send( "set_#{association_name}_target", assocated_records_by_primary_key[record.send(options[:foreign_key])] ) end finder.size end
Notice my choice of naming - somehow the phrase “preload associations” has escaped my initial round of searching, but as
soon as I went to implement something, it popped to the forefront of my consciousness. It took a few months before I
inadvertently stumbled across Rails’ preload_associations
and embarked on another - more fruitful - search.