Eager-loading polymorphic associations

Updated . Posted . Visible to the public.

To avoid n+1 queries, you want to eager-load associated records if you know you need to access them later on.

The Rails docs say:

Eager loading is supported with polymorphic associations.

This is true, but has some caveats.

Example

Consider the following models:

class Image < ActiveRecord::Base; end
class Video < ActiveRecord::Base; end
class PageVersion < ActiveRecord::Base
  belongs_to :primary_medium, polymorphic: true # may be Image or Video
end
class Page < ActiveRecord::Base
  belongs_to :current_version, class_name: 'PageVersion'
end

Now, when you load pages and include their current versions' primary media...

Page.includes(:current_version => :primary_medium).to_a

... Rails will produce 4 queries:

Page Load (0.7ms) SELECT "pages".* FROM "pages"
PageVersion Load (0.7ms) SELECT "page_versions".* FROM "page_versions" WHERE "page_versions"."id" IN (1, 2, 3)
Image Load (0.4ms) SELECT "images".* FROM "images" WHERE "images"."id" IN (1, 2)
Video Load (0.5ms) SELECT "videos".* FROM "videos" WHERE "videos"."id" IN (1)

Accessing any page.current_version.primary_medium will then happen without any extra queries.

Be careful when adding conditions

However, eager loading will fail if you add conditions on an association that owns polymorphic associations which you want to include.
For example, the following raises an error.

Page.includes(:current_version => :primary_medium).order('page_versions.updated_at').to_a
# => ActiveRecord::EagerLoadPolymorphicError: Cannot eagerly load the polymorphic association :primary_medium

Usually, Rails will automatically JOIN your includes so that your order or where statements work as expected. This is exactly why it can no longer eager-load the polymorphic association.

How to eager-load polymorphic associations AND add conditions

Fix that error by explicitly joining the table that you want to order on.

Page.includes(:current_version => :primary_medium).joins(:current_version).order('page_versions.updated_at').to_a

Rails will again produce 4 queries for that (a bit more complex because of ordering):

Page Load (0.9ms)  SELECT "pages".* FROM "pages" INNER JOIN "page_versions" ON "page_versions"."id" = "pages"."current_version_id"  ORDER BY page_versions.updated_at
PageVersion Load (0.4ms)  SELECT "page_versions".* FROM "page_versions" WHERE "page_versions"."id" IN (1, 2, 3)
Image Load (0.4ms)  SELECT "images".* FROM "images" WHERE "images"."id" IN (1, 2)
Video Load (0.4ms)  SELECT "videos".* FROM "videos" WHERE "videos"."id" IN (1)

Using preload instead of includes

Note that if you had used preload instead of includes, Rails would not have magically converted to a JOIN:

Page.preload(:current_version => :primary_medium).order('page_versions.updated_at').to_a

That raises an error because the page_versions table was not joined:

ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "page_versions"

Joining (like above with includes) works as expected:

Page.preload(:current_version => :primary_medium).joins(:current_version).order('page_versions.updated_at').to_a

Using Edge Rider

Alternatively, instead of using includes at all, you can use preload_association from Edge Rider Show archive.org snapshot after loading your records:

pages = Page.joins(:current_version).order('page_versions.updated_at').to_a
Page.preload_associations(pages, :current_version => :primary_medium)
Arne Hartherz
Last edit
Felix Eschey
License
Source code in this card is licensed under the MIT License.
Posted by Arne Hartherz to makandra dev (2014-09-30 07:18)