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)