At a certain point in the growth of your application's object model, you'll likely employ single table inheritence or polymorphism. Asscociations involving either of these concepts require more configuration and unfortunately, often more hair-pulling.

One particular snag is that when you query a collection of objects that don't all have the same associations, you can't simply eager load them, resulting in a barrage of N+1 queries.

Preloading STI models

Given an STI Animal model where a Kangeroo is the only type that has a :pouch association:

class Animal < ApplicationRecord
end

class Kangeroo < Animal
  has_one :pouch
end

Trying to reference the pouch association for any Kangeroo in the set of Animals would result in an N+1:

SELECT  "animals".* FROM "animals" ORDER BY "animals"."id" ASC LIMIT 10
SELECT  "pouches".* FROM "pouches" WHERE "pouches"."id" = 1 LIMIT 1
SELECT  "pouches".* FROM "pouches" WHERE "pouches"."id" = 2 LIMIT 1
SELECT  "pouches".* FROM "pouches" WHERE "pouches"."id" = 3 LIMIT 1
SELECT  "pouches".* FROM "pouches" WHERE "pouches"."id" = 4 LIMIT 1

Preloading via Animal.preload(:pouch) presumes a pouch association exists on all the other Animal subclasses, which does not. We need to take one step closer to the groundwork of what happens when you call ordinary methods includes or preload on an ActiveRecord::Relation.

Enter ActiveRecord::Associations::Preloader. An instance of it exposes a method, #preload, which first takes a collection, and then associations_to_preload as a hash or single symbol, just like what you'd pass to includes or preload.

Pass instances of Animal that implement :pouch, and then literal symbol :pouch to roll up the N+1 issue:

animals = Animal.limit(10)

pl = ActiveRecord::Associations::Preloader.new
pl.preload(animals.select { |a| a.respond_to?(:pouch) }, :pouch)

animals.select { |a| a.is_a?(Kangeroo) }.map(&:pouch) # No N+1 query

The SQL output of that would be:

SELECT  "animals".* FROM "animals" ORDER BY "animals"."id" ASC LIMIT 10
SELECT "pouches".* FROM "pouches" WHERE "pouches"."id" IN (1, 2, 3, 4)

Preloading associations of polymorphic associations

Address the polymorphic version of this problem in pretty much the same way. Consider an Animal associated to a species, which, for the sake of a silly example, is a holding place for general data.

class Animal < ApplicationRecord
  belongs_to :species, polymorphic: true
end

class ReptileSpecies
  has_many :animals
  has_many :scales
end

class BirdSpecies
  has_many :animals
  has_many :wings
end

To preload all of this data, handle each identity individually, similarly to above:

animals = Animal.preload(:species).limit(10)

pl = ActiveRecord::Associations::Preloader.new
pl.preload(animals.select { |a| a.species_type == 'ReptileSpecies' }, species: :scales)
pl.preload(animals.select { |a| a.species_type == 'BirdsSpecies' }, species: :wings)

animals.map { |a| a.species.try(:scales) } # No N+1 query
animals.map { |a| a.species.try(:wings) } # No N+1 query

Note that the data path given as the second argument still starts at the Animal level, so we need to step through :species first.

In summary, we query records with STI or polymorphic qualities and load them into memory, and group them by whatever defines their unique set of associations. Then, we feed each group into the Preloader and give it a path to the data to query.

More blog posts