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.