Posted over 6 years ago. Visible to the public. Linked content.

Eager-loading polymorphic associations

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:

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

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

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

... Rails will produce 4 queries:

Copy
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.

Copy
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.

Copy
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):

Copy
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:

Copy
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:

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

Joining (like above with includes) works as expected:

Copy
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 after loading your records:

Copy
pages = Page.joins(:current_version).order('page_versions.updated_at').to_a Page.preload_associations(pages, :current_version => :primary_medium)

By refactoring problematic code and creating automated tests, makandra can vastly improve the maintainability of your Rails application.

Owner of this card:

Avatar
Arne Hartherz
Last edit:
about 1 month ago
by Judith Roth
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Arne Hartherz to makandra dev
This website uses short-lived cookies to improve usability.
Accept or learn more