bullet icon indicating copy to clipboard operation
bullet copied to clipboard

Incorrectly tells me to eager load intermediate association when eager loading a `has_one` through

Open JoshCheek opened this issue 2 years ago • 0 comments

Explanation

I'm using Rails 6 and have a has_one through association. Rails implemented my includes in my app as an eager_load, I have reproduced it here by directly calling eager_load. It gives me the result I want, with a single query. It is what I expected it to be. Bullet, considers it an N+1 query, and wants me to includes the join table.

This seems like a bug to me.

Possibly related to https://github.com/flyerhzm/bullet/pull/341

Example

# ===== SETUP THE GEMS =====
require 'bundler/inline'
gemfile do
  source 'https://rubygems.org'
  gem 'activerecord', '=6.1.4.1'
  gem 'bullet',       '=6.1.5'
  gem 'sqlite3',      '=1.4.2'
end

require 'active_record'
require 'logger'
require 'bullet'
require 'bullet/active_record61'

ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
ActiveSupport::LogSubscriber.colorize_logging = false

# ===== DEFINE THE SCHEMA / MODELS =====
ActiveRecord::Schema.define do
  self.verbose = false
  
  create_table :lefts do |t|
    t.string :name
  end

  create_table :middles do |t|
    t.integer :left_id
    t.integer :right_id
  end
  
  create_table :rights do |t|
    t.string :name
  end
end

class Left < ActiveRecord::Base
  has_one :middle
  has_one :right, through: :middle
end

class Middle < ActiveRecord::Base
  belongs_to :left
  belongs_to :right
end

class Right < ActiveRecord::Base
end

# ===== CREATE TEST DATA =====
Left.create! name: 'l1', right: Right.new(name: 'r1')
Left.create! name: 'l2', right: Right.new(name: 'r2')

# ===== TURN ON BULLET + LOGGING =====
Bullet.enable = Bullet.raise = true
Bullet.start_request
ActiveRecord::Base.logger = Logger.new $stdout

# ===== THE APP CODE =====
Left.eager_load(:right).all.each do |left|
  left.right.name # => "r1", "r2"
end

# ===== BULLET CONSIDERS IT WRONG =====
Bullet.to_enum(:for_each_active_notifier_with_notification).first
# => #<Bullet::Notification::NPlusOneQuery:0x0000000142606808
#     @associations=[:middle],
#     @base_class="Left",
#     @callers=
#      ["program.rb:60:in `block in <main>'", "program.rb:59:in `<main>'"],
#     @notifier=UniformNotifier::Raise,
#     @path=nil>

Bullet.perform_out_of_channel_notifications # ~> Bullet::Notification::UnoptimizedQueryError: user: josh\n \nUSE eager loading detected\n  Left => [:middle]\n  Add to your query: .includes([:middle])\nCall stack\n  program.rb:60:in `block in <main>'\n  program.rb:59:in `<main>'\n\n

# >> D, [2021-10-19T20:08:49.500942 #71323] DEBUG -- :   SQL (0.1ms)  SELECT "lefts"."id" AS t0_r0, "lefts"."name" AS t0_r1, "rights"."id" AS t1_r0, "rights"."name" AS t1_r1 FROM "lefts" LEFT OUTER JOIN "middles" ON "middles"."left_id" = "lefts"."id" LEFT OUTER JOIN "rights" ON "rights"."id" = "middles"."right_id"

# ~> Bullet::Notification::UnoptimizedQueryError
# ~> user: josh
# ~>  
# ~> USE eager loading detected
# ~>   Left => [:middle]
# ~>   Add to your query: .includes([:middle])
# ~> Call stack
# ~>   program.rb:60:in `block in <main>'
# ~>   program.rb:59:in `<main>'
# ~> 
# ~>
# ~> /Users/josh/.gem/ruby/3.0.2/gems/uniform_notifier-1.14.2/lib/uniform_notifier/raise.rb:19:in `_out_of_channel_notify'
# ~> /Users/josh/.gem/ruby/3.0.2/gems/uniform_notifier-1.14.2/lib/uniform_notifier/base.rb:25:in `out_of_channel_notify'
# ~> /Users/josh/.gem/ruby/3.0.2/gems/bullet-6.1.5/lib/bullet/notification/base.rb:50:in `notify_out_of_channel'
# ~> /Users/josh/.gem/ruby/3.0.2/gems/bullet-6.1.5/lib/bullet.rb:237:in `block in perform_out_of_channel_notifications'
# ~> /Users/josh/.gem/ruby/3.0.2/gems/bullet-6.1.5/lib/bullet.rb:299:in `block (2 levels) in for_each_active_notifier_with_notification'
# ~> /Users/josh/.rubies/ruby-3.0.2/lib/ruby/3.0.0/set.rb:344:in `each_key'
# ~> /Users/josh/.rubies/ruby-3.0.2/lib/ruby/3.0.0/set.rb:344:in `each'
# ~> /Users/josh/.gem/ruby/3.0.2/gems/bullet-6.1.5/lib/bullet.rb:297:in `block in for_each_active_notifier_with_notification'
# ~> /Users/josh/.gem/ruby/3.0.2/gems/bullet-6.1.5/lib/bullet.rb:296:in `each'
# ~> /Users/josh/.gem/ruby/3.0.2/gems/bullet-6.1.5/lib/bullet.rb:296:in `for_each_active_notifier_with_notification'
# ~> /Users/josh/.gem/ruby/3.0.2/gems/bullet-6.1.5/lib/bullet.rb:235:in `perform_out_of_channel_notifications'
# ~> program.rb:67:in `<main>'

JoshCheek avatar Oct 20 '21 01:10 JoshCheek