djezzzl/n1_loader

fail with nested preloading using rails v7.0

Closed this issue · 2 comments

Hello, thanks for this gem!

I've been using the n1_loader gem (version 1.6.3) in my Ruby on Rails application and have run into an issue when using nested preloading. Whenever I try to use nested preloading like in the bug report, it throws a NoMethodError with the message "undefined method `future_classes' for N1Loader::LoaderCollection".

Bug report:

require 'bundler/inline'

gemfile(true) do
  source 'https://rubygems.org'

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem 'activerecord', '~> 7.0.0'
  gem 'n1_loader', '1.6.3', require: 'n1_loader/active_record'
  gem 'sqlite3'
end

require 'active_record'
require 'minitest/autorun'
require 'logger'

ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Base.logger = Logger.new($stdout)

ActiveRecord::Schema.define do
  create_table :authors, force: true do |t|
  end

  create_table :posts, force: true do |t|
    t.integer :author_id
    t.text :body
  end

  create_table :comments, force: true do |t|
    t.integer :post_id
  end
end

class Author < ActiveRecord::Base
  has_many :posts
end

class Post < ActiveRecord::Base
  has_many :comments
  belongs_to :author
  n1_optimized :lines_count do |posts|
    posts.each do |post|
      fulfill(post, post.body.lines.count)
    end
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

class BugTest < Minitest::Test
  def test_nested_preloading
    author = Author.create!
    post = Post.create!(author:, body: "body\n123")
    comment = Comment.create!(post:)

    assert_equal comment, Comment.preload(:post).first
    assert_equal comment, Comment.preload(post: %i[author]).first
    assert_equal comment, Comment.preload(post: %i[lines_count]).first
    assert_equal comment, Comment.preload(post: %i[author lines_count]).first # fail here
    assert_equal comment.post.lines_count, 2
  end
end

The bug report results:

  1) Error:
BugTest#test_future_preloading:
NoMethodError: undefined method `future_classes' for #<N1Loader::LoaderCollection:0x00000001135e5ad8 @loader_class=#<Class:0x0000000113444c60>, @elements=[#<Post id: 1, author_id: 1, body: "body\n123">]>

            loaders.flat_map(&:future_classes).uniq
                   ^^^^^^^^^
    /Users/igor.gonchar/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.4.2/lib/active_record/associations/preloader/branch.rb:27:in `each'
    /Users/igor.gonchar/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.4.2/lib/active_record/associations/preloader/branch.rb:27:in `flat_map'
    /Users/igor.gonchar/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.4.2/lib/active_record/associations/preloader/branch.rb:27:in `immediate_future_classes'
    /Users/igor.gonchar/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.4.2/lib/active_record/associations/preloader/branch.rb:22:in `future_classes'
    /Users/igor.gonchar/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.4.2/lib/active_record/associations/preloader/batch.rb:21:in `block in call'
...

Environment:

Ruby version: 3.1.2
Rails version: 7.0.4.2
n1_loader version: 1.6.3

Additional Information:
I have fixed this error by defining the future_classes method like this.

N1Loader::LoaderCollection.define_method :future_classes do
   []
end

Is it the right solution?

Please let me know if there is any additional information I can provide to help resolve this issue. Thank you.

Hi @gigorok,

Thank you for using the gem and raising the issue! I'm sorry it took me long to answer.

Looking at the issue above, the fix seems correct. Would you mind opening a PR? Please, let me know if you can't, so I will allocate some time and investigate it myself.

Hi @gigorok,

I'm sorry that I forgot about this for a long time. It's finally here: #39

I released it under 1.6.5.

Thank you again, and have a great day!

P.S. Your issue with well-structured minimal reproducible code is impressive and appreciated!