Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eager loading doesn't respect prior #includes call #26586

Closed
jrdioko opened this issue Sep 22, 2016 · 7 comments
Closed

Eager loading doesn't respect prior #includes call #26586

jrdioko opened this issue Sep 22, 2016 · 7 comments

Comments

@jrdioko
Copy link

jrdioko commented Sep 22, 2016

Using #includes to eager load a model that has already been eager loaded in a previous step causes ActiveRecord to unnecessarily fetch the models again. This means that eager loading must be done at a single top-level place in the code, rather than being able to add it to all the specific lower-level places where it is required (just like .transaction calls can be added anywhere a transaction is required, and they will all join the parent transaction). See the example below:

Steps to reproduce

Consider a simple case with three levels of associations:

class Foo
  has_many :bars
end

class Bar
  belongs_to :foo
  has_one :baz
end

class Baz
  belongs_to :bar
end

This call illustrates the issue:

Foo.all.includes(bars: :baz).each do |foo|
  foo.bars.includes(:baz).each { |bar| do_something }
end

Expected behavior

The includes(bars: :baz) call should execute a database query to load bars and a query to load bazs. The includes(:baz) calls should do nothing, since bazs have already been eager loaded.

Actual behavior

Each includes(:baz) call (one per iteration in the loop) executes separate eager loading queries, despite the fact that bazs have already been eager loaded before entering the loop.

System configuration

Rails version: 4.2.7.1

Ruby version: 2.3.1

@mechanicles
Copy link
Contributor

It happens in Rails 5 too. But I feel this is a correct flow. We don't get any clue about the outer includes while the inner includes is being performed. Both have different scopes/contexts I guess. Inner includes looks redundant to me. Why would anyone do that?

@jrdioko
Copy link
Author

jrdioko commented Sep 27, 2016

For the same reason that you'd wrap something in a transaction even though the parent call also happens to be in a transaction several layers up: keeping the call close to the code that must be transactional ensures that that code is always protected, regardless of how the calling code changes or the called code is used in different places in the future.

For the includes case, imagine a low-level helper method that eager loads an association and does something with it. It makes sense to put the includes call in that helper rather than repeated in many places that construct the initial relation and call the helper. But this behavior means that if any of those places also happen to eager load the same association (for some other purpose than what the helper is doing), duplicate queries will occur.

In general, it seems to be restrict code re-use in a complex application. High-level methods doing eager loading need to ensure that no lower-level methods ever repeat the same eager loading on the same relation object. And low-level methods adding eager loading to a passed relation object need to ensure that nowhere up the chain also added the same eager loading. Considering how transaction works, it surprised me that includes is not aware of when an association has already been included.

@stale
Copy link

stale bot commented Mar 27, 2017

This issue has been automatically marked as stale because it has not been commented on for at least three months.
The resources of the Rails team are limited, and so we are asking for your help.
If you can still reproduce this error on the 5-1-stable branch or on master, please reply with all of the information you have about it in order to keep the issue open.
Thank you for all your contributions.

@stale stale bot closed this as completed Apr 3, 2017
@jrdioko
Copy link
Author

jrdioko commented May 16, 2017

I just created a new test Rails app (on 5.1.1) and confirmed this issue still exists. With the setup mentioned above, running the call generates the following SQL:

  Foo Load (0.1ms)  SELECT "foos".* FROM "foos"
  Bar Load (0.2ms)  SELECT "bars".* FROM "bars" WHERE "bars"."foo_id" = 1
  Baz Load (0.2ms)  SELECT "bazs".* FROM "bazs" WHERE "bazs"."bar_id" = 1
  Bar Load (0.2ms)  SELECT "bars".* FROM "bars" WHERE "bars"."foo_id" = ?  [["foo_id", 1]]
  Baz Load (0.1ms)  SELECT "bazs".* FROM "bazs" WHERE "bazs"."bar_id" = 1

Notice that the bars and bazs are eager loaded twice.

@rails-bot rails-bot bot removed the stale label May 16, 2017
@rafaelfranca
Copy link
Member

Could you create a reproduction script with this template https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb?

@jrdioko
Copy link
Author

jrdioko commented May 17, 2017

@matthewd
Copy link
Member

This is known behaviour; includes always returns a new (unpopulated) Relation instance. Making it smarter would be nice, but it would be more feature request than bug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants