salsify / goldiloader

Just the right amount of Rails eager loading

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Can't auto eager load has_many through association with a scope that uses includes

jturkel opened this issue · comments

Consider the following models:

class Blog < ActiveRecord::Base
  has_many :posts
  has_many :authors, -> { includes(:address).where('addresses.city IS NOT NULL').references(:address) }, through: :posts
end

class Post < ActiveRecord::Base
  belongs_to :blog
  belongs_to :author
end

class Author < ActiveRecord::Base
  has_many :posts
  has_one :address
end

class Address < ActiveRecord::Base
  belongs_to :author
end

Attempting to auto eager load (or regular eager load) Blog#authors results in the following error:

ActiveRecord::ConfigurationError: Association named 'address' was not found on Post; perhaps you misspelled it?

Underlying Rails bug is fixed by rails/rails#12725. Workaround in Golidloader is to set auto_include = false.

The context in the scope for has_many through associations incorrectly points to the source association rather than the target association when eager loading. This breaks eager loading of most has_many through associations that use scopes.

I've confirmed this problem still exists in Rails master. Reproducible test case can be found here: https://gist.github.com/jturkel/fc4cca3949724da4b902

Looks like there's a new Rails PR up to fix this issue: rails/rails#23100

is this still an issue?

@NullVoxPopuli - Yes, it's still an issue. The Rails bug still seems to be present in Rails 5.1.2. See this gist.

Encountered the same problem (ruby 2.4.1, Rails 5.1.4). Only I do not have any includes in the scope

class Blog < ApplicationRecord
  has_many :posts
  has_many :authors, -> { where name: 'James' }, through: :posts
end

class Post < ApplicationRecord
  belongs_to :author
  belongs_to :blog
end

class Author < ApplicationRecord
  has_many :posts
end

Of course, I started with something more complex in the scope, only when debugging, I found, that it didn't really matter what I put in there as long as it shows up in the where clause...

So, when I query Blog.includes(:authors), it tries the following lines of SQL:

SELECT `blogs`.* FROM `blogs`
SELECT `posts`.* FROM `posts` WHERE `authors`.`name` = 'James'
  AND `posts`.`bog_id` IN (1, 2)

Where the clause authors.name = 'James' does not make sense for mysql, since it made no queries to the table authors.

Also, if I remove the scope from has_many :authors, the query above runs the same two line (except for the where clause, of course), and the authors table is not even queried until I really want something with them (like Blog.includes(:authors).map { |blog| blog.authors.map &:name }). Then it runs one extra query:

SELECT `authors`.* FROM `authors` WHERE `authors`.`id` IN (1, 2)

Lastly, if I put the scope on the join association

class Blog < ApplicationRecord
  has_many :posts
  has_many :posts_of_james, -> { joins(:author).where(authors: {name: 'James'}) }, class_name: 'Post'
  has_many :authors, through: :posts_of_james
end

it runs the inner join query missing from the first approach.

SELECT `posts`.* FROM `posts` INNER JOIN `authors`
  ON `authors`.`id` = `posts`.`author_id`
  WHERE (`authors`.`name` = 'James') AND `posts`.`id` IN (1, 2)
SELECT `authors`.* FROM `authors` WHERE `authors`.`id` IN (1, 2)

This has been fixed in Rails 5.2. See this gist for a reproducible test case.