diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 167bd8c82c..3393534eb3 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,41 @@ +* Fix nested `has_many :through` associations on unpersisted parent instances. + + For example, if you have + + class Post < ActiveRecord::Base + belongs_to :author + has_many :books, through: :author + has_many :subscriptions, through: :books + end + + class Author < ActiveRecord::Base + has_one :post + has_many :books + has_many :subscriptions, through: :books + end + + class Book < ActiveRecord::Base + belongs_to :author + has_many :subscriptions + end + + class Subscription < ActiveRecord::Base + belongs_to :book + end + + Before: + + If `post` is not persisted, then `post.subscriptions` will be empty. + + After: + + If `post` is not persisted, then `post.subscriptions` can be set and used + just like it would if `post` were persisted. + + Fixes #16313. + + *Zoltan Kiss* + * Fixed inconsistency with `first(n)` when used with `limit()`. The `first(n)` finder now respects the `limit()`, making it consistent with `relation.to_a.first(n)`, and also with the behavior of `last(n)`. diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 249eb1775b..806397f83a 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -68,7 +68,7 @@ def stale_state end def foreign_key_present? - through_reflection.belongs_to? && !owner[through_reflection.foreign_key].nil? + through_reflection.belongs_to_or_through? && !owner[through_reflection.foreign_key].nil? end def ensure_mutable diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 9e32b69786..7b53f9c15c 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -504,6 +504,10 @@ def clear_association_scope_cache # :nodoc: @association_scope_cache.clear end + def belongs_to_or_through? + belongs_to? + end + def nested? false end @@ -836,6 +840,10 @@ def join_scopes(table, predicate_builder) # :nodoc: source_reflection.join_scopes(table, predicate_builder) + super end + def belongs_to_or_through? + through_reflection.belongs_to_or_through? + end + def has_scope? scope || options[:source_type] || source_reflection.has_scope? || diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 7c9c9e81ab..a19cd9d365 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -1308,6 +1308,35 @@ def test_incorrectly_ordered_through_associations end end + def test_single_has_many_through_association_with_unpersisted_parent_instance + post_with_single_has_many_through = Class.new(Post) do + def self.name; "PostWithSingleHasManyThrough"; end + has_many :subscriptions, through: :author + end + post = post_with_single_has_many_through.new + post.author = Author.create!(name: "Federico Morissette") + book = Book.create!(name: "essays on single has many through associations") + post.author.books << book + subscription = Subscription.first + book.subscriptions << subscription + assert_equal [subscription], post.subscriptions.to_a + end + + def test_nested_has_many_through_association_with_unpersisted_parent_instance + post_with_nested_has_many_through = Class.new(Post) do + def self.name; "PostWithNestedHasManyThrough"; end + has_many :books, through: :author + has_many :subscriptions, through: :books + end + post = post_with_nested_has_many_through.new + post.author = Author.create!(name: "Obie Weissnat") + book = Book.create!(name: "essays on nested has many through associations") + post.author.books << book + subscription = Subscription.first + book.subscriptions << subscription + assert_equal [subscription], post.subscriptions.to_a + end + private def make_model(name) Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }