Delegated Type associations do not cascade
Steps to reproduce
Consider the following:
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails", "~> 6.1"
gem "sqlite3", platform: :mri
end
require "active_record"
require "minitest/autorun"
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|
t.string :name
end
create_table :books, force: true do |t|
t.references :author, null: false
t.references :book_details, polymorphic: true, null: false
t.string :title
end
create_table :nonfiction_books, force: true do |t|
t.string :subject_area, null: false
end
end
class Author < ActiveRecord::Base
has_many :books, inverse_of: :author
end
class Book < ActiveRecord::Base
belongs_to :author, inverse_of: :books
delegated_type :book_details, types: %w[NonfictionBook], dependent: :destroy
end
class NonfictionBook < ActiveRecord::Base
has_one :book, as: :book_details
has_one :author, through: :book
end
class BugTest < Minitest::Test
def test_nonfiction_book_has_author
author = Author.new(name: "Author")
book = Book.new(author: author, title: "Book")
nonfiction_book = NonfictionBook.new(book: book, subject_area: "Ruby on Rails")
assert_equal book.author, author
assert_equal nonfiction_book.author, author
end
end
Expected behavior
Having assigned an author to the book, I expect to be able to ask the book details about its author.
Actual behavior
The book's details have a nil author.
System configuration
Rails version: "~> 6.1"
Ruby version: "ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin20]"
This is surprising:
assert_equal book.author, author
assert_equal nonfiction_book.book, book
assert_equal nonfiction_book.book.author, author
assert_equal nonfiction_book.author, author # this fails, above assertions pass
Agreed - now I know that this is the behaviour I can sort of reason why, but it's definitely surprising.
This also passes, and matches the delegated types docs a bit more closely:
def test_nonfiction_book_has_author
author = Author.create!(name: "Author")
nonfiction_book = NonfictionBook.new(subject_area: "Ruby on Rails")
book = Book.create!(author: author, title: "Book", book_details: nonfiction_book)
assert_equal book.author, author
assert_equal nonfiction_book.book, book
assert_equal nonfiction_book.book.author, author
assert_equal nonfiction_book.author, author
end
I agree it's weird though. Haven't quite figured out what's going on, but I don't think this is being called and it probably should be.
@intrip This seems similar to #42494, wdyt?
@zzak yes looks like a similar issue but with a different subject.
I'll try to write a PR to fix that unless somebody else is already on it.
I'm not working on this, it's all yours
@zzak @ghiculescu https://github.com/rails/rails/pull/42575 fixes the issue.
Also encountering this bug.
just noting another scenario that should be resolved by the same fix:
A.has_one :c, through: :b
a = A.includes(b: :c).first
a.c # should be preloaded/cached, but it causes a DB query!
NOTE: using A.delegate :c, to: :b instead of has_one does not have this problem, but then we lose the benefits of has_one (e.g. joins)
full reproduction:
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails", "~> 8"
gem "sqlite3", platform: :mri
end
require "active_record"
require "minitest/autorun"
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|
t.string :name
end
create_table :books, force: true do |t|
t.references :author, null: false
t.string :title
end
create_table :chapters, force: true do |t|
t.references :book, null: false
t.string :title
end
end
class Author < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :author
has_many :chapters
end
class Chapter < ActiveRecord::Base
belongs_to :book
has_one :author, through: :book # this is the line in question!
end
class BugTest < Minitest::Test
def test_includes_loads_the_cached_through_association
author = Author.create!(name: "My Author")
book = Book.create!(author: author, title: "My Book")
Chapter.create!(book: book, title: "Chapter 1")
chapter = Chapter.includes(book: :author).sole
# `chapter.author` should be already loaded
assert chapter.association_cached?(:author)
end
def test_nonfiction_book_has_author
author = Author.new(name: "My Author")
book = Book.new(author: author, title: "My Book")
chapter = Chapter.new(book: book, title: "Chapter 1")
assert_equal book.author, author
assert_equal chapter.author, author
end
end
This is an issue for me in Rails 8.0.3. Referenced PR is not merged so I'll work around it!