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

has_one :through associations ignore preloaded records #51817

Open
grncdr opened this issue May 14, 2024 · 0 comments
Open

has_one :through associations ignore preloaded records #51817

grncdr opened this issue May 14, 2024 · 0 comments

Comments

@grncdr
Copy link
Contributor

grncdr commented May 14, 2024

Steps to reproduce

# frozen_string_literal: true

require "bundler/inline"

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

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

  gem "rails", github: "rails/rails", branch: "main"
  gem "sqlite3"
end

require "active_record"
require "active_support/testing/assertions"
require "active_record/testing/query_assertions"
require "minitest/autorun"
require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :services, force: true do |t|
    t.string :name, null: false
  end

  create_table :service_logs, force: true do |t|
    t.references :service, null: false, foreign_key: true
  end

  create_table :charges, force: true do |t|
    t.references :service_log, null: false, foreign_key: true
  end
end

class Service < ActiveRecord::Base
  has_many :service_logs
end

class ServiceLog < ActiveRecord::Base
  belongs_to :service, inverse_of: :service_logs
  has_one :charge
end

class Charge < ActiveRecord::Base
  belongs_to :service_log, inverse_of: :charge
  has_one :service, through: :service_log
end

class BugTest < Minitest::Test
  include ActiveSupport::Testing::Assertions
  include ActiveRecord::Assertions::QueryAssertions

  def test_has_one_through_uses_preloaded_record
    service = Service.create!(name: 'Foobar')
    service_log = ServiceLog.create!(service:)
    charge = Charge.new(service_log: service_log)

    # this works
    assert_no_queries { charge.service_log.service }

    # this fails
    assert_no_queries { charge.service }
  end
end

Expected behavior

I'd expect that has_one through: ... defines a method that uses an already loaded join record if possible, to avoid redundant (and often N+1) queries).

Actual behavior

The generated method will load the associated record directly, even if it has already been preloaded in the join model.

I've tried to keep the test case above super clear, but my actual use case looks something like this:

charges = ServiceLog.where(...).preload(:service).each do |service_log|
  charge = Charge.new(service_log:)
  do_something if charge.service.some_predicate?
  charge.save!
end

The preload loads the small number of Service records involved, but accessing Charge#service causes N+1 queries as each charge loads the service independently again.

System configuration

Rails version: main, 7.0, 7.1

Ruby version: 3.1.4

@grncdr grncdr changed the title has_one :through associations ignore preloaded record has_one :through associations ignore preloaded records May 14, 2024
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

2 participants