rails icon indicating copy to clipboard operation
rails copied to clipboard

Delegated Type associations do not cascade

Open ball-hayden opened this issue 4 years ago • 10 comments

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]"

ball-hayden avatar Jun 04 '21 10:06 ball-hayden

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

ghiculescu avatar Jun 04 '21 15:06 ghiculescu

Agreed - now I know that this is the behaviour I can sort of reason why, but it's definitely surprising.

ball-hayden avatar Jun 04 '21 15:06 ball-hayden

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.

ghiculescu avatar Jun 04 '21 15:06 ghiculescu

@intrip This seems similar to #42494, wdyt?

zzak avatar Jun 18 '21 06:06 zzak

@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.

intrip avatar Jun 18 '21 08:06 intrip

I'm not working on this, it's all yours

ghiculescu avatar Jun 18 '21 14:06 ghiculescu

@zzak @ghiculescu https://github.com/rails/rails/pull/42575 fixes the issue.

intrip avatar Jun 23 '21 12:06 intrip

Also encountering this bug.

arjun810 avatar Mar 18 '25 23:03 arjun810

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

wyattades avatar Nov 20 '25 18:11 wyattades

This is an issue for me in Rails 8.0.3. Referenced PR is not merged so I'll work around it!

jeropaul avatar Dec 09 '25 00:12 jeropaul