Home

Sep 11

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.

blog comments powered by Disqus