Skip to content

Commit 45cb290

Browse files
authored
Merge pull request rails#53957 from joshuay03/address-through-record-setting-edge-cases
Handle nested middle belongs_to, not-loaded target when setting through records
2 parents bbd7585 + a0a8dd1 commit 45cb290

File tree

10 files changed

+106
-18
lines changed

10 files changed

+106
-18
lines changed

activerecord/lib/active_record/associations/association.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@ def load_target
193193
reflections.pop
194194
reflections.reverse!
195195

196-
@target = reflections.reduce(through_association.target) do |middle_target, reflection|
196+
@target = reflections.reduce(through_association.target) do |middle_target, through_reflection|
197197
break unless middle_target
198-
middle_target.association(reflection.source_reflection_name).target
198+
middle_target.association(through_reflection.source_reflection_name).load_target
199199
end
200200
end
201201

activerecord/lib/active_record/associations/collection_association.rb

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -273,17 +273,15 @@ def load_target
273273
if find_target?
274274
@target = merge_target_lists(find_target, target)
275275
elsif target.empty? && set_through_target_for_new_record?
276-
reflections = reflection.chain
277-
reflections.pop
278-
reflections.reverse!
276+
reflections = reflection.chain.reverse!
279277

280-
@target = reflections.reduce(through_association.target) do |middle_target, reflection|
281-
if middle_target.empty?
278+
@target = reflections.each_cons(2).reduce(through_association.target) do |middle_target, (middle_reflection, through_reflection)|
279+
if middle_target.nil? || (middle_reflection.collection? && middle_target.empty?)
282280
break []
283-
elsif reflection.collection?
284-
middle_target.flat_map { |record| record.association(reflection.source_reflection_name).target }
281+
elsif middle_reflection.collection?
282+
middle_target.flat_map { |record| record.association(through_reflection.source_reflection_name).load_target }
285283
else
286-
middle_target.association(reflection.source_reflection_name).target
284+
middle_target.association(through_reflection.source_reflection_name).load_target
287285
end
288286
end
289287
end

activerecord/lib/active_record/reflection.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,10 @@ def source_reflection
651651
self
652652
end
653653

654+
def source_reflection_name
655+
source_reflection.name
656+
end
657+
654658
# A chain of reflections from this one back to the owner. For more see the explanation in
655659
# ThroughReflection.
656660
def collect_join_chain

activerecord/test/cases/associations/has_many_through_associations_test.rb

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,20 @@ def setup
5757
Reader.create person_id: 0, post_id: 0
5858
end
5959

60-
def test_setting_association_on_new_record_sets_through_records
60+
def test_setting_has_many_through_one_association_on_new_record_sets_through_records
61+
account_1, account_2 = Account.create!(credit_limit: 100), Account.create!(credit_limit: 100)
62+
firm = Firm.new(accounts: [account_1, account_2])
63+
client = Client.new
64+
client.firm = firm
65+
66+
assert_predicate account_1, :persisted?
67+
assert_predicate account_2, :persisted?
68+
assert_predicate client, :new_record?
69+
assert_predicate client.firm, :new_record?
70+
assert_no_queries { assert_equal [account_1, account_2].sort, client.accounts.sort }
71+
end
72+
73+
def test_setting_has_many_through_many_association_on_new_record_sets_through_records
6174
subscriber_1, subscriber_2 = Subscriber.create!(nick: "nick 1"), Subscriber.create!(nick: "nick 2")
6275
subscription_1 = Subscription.new(subscriber: subscriber_1)
6376
subscription_2 = Subscription.new(subscriber: subscriber_2)
@@ -68,10 +81,42 @@ def test_setting_association_on_new_record_sets_through_records
6881
assert_predicate subscriber_2, :persisted?
6982
assert_predicate book, :new_record?
7083
book.subscriptions.each { |subscription| assert_predicate subscription, :new_record? }
71-
assert_equal [subscriber_1, subscriber_2].sort, book.subscribers.sort
72-
end
73-
74-
def test_setting_association_on_new_record_sets_nested_through_records
84+
assert_no_queries { assert_equal [subscriber_1, subscriber_2].sort, book.subscribers.sort }
85+
end
86+
87+
def test_setting_nested_has_many_through_one_association_on_new_record_sets_nested_through_records
88+
post_tagging_1, post_tagging_2 = Tagging.create!, Tagging.create!
89+
post = Post.create!(title: "Tagged", body: "Post", taggings: [post_tagging_1, post_tagging_2])
90+
author = Author.new(name: "Josh")
91+
author.posts = [post]
92+
categorization = Categorization.new
93+
categorization.author = author
94+
95+
assert_predicate post_tagging_1, :persisted?
96+
assert_predicate post_tagging_2, :persisted?
97+
assert_predicate post, :persisted?
98+
assert_predicate categorization, :new_record?
99+
assert_predicate categorization.author, :new_record?
100+
assert_no_queries { assert_equal [post_tagging_1, post_tagging_2].sort, categorization.post_taggings.sort }
101+
end
102+
103+
def test_setting_nested_has_many_through_one_association_on_new_record_sets_targetless_nested_through_records
104+
post = Post.create!(title: "Tagged", body: "Post")
105+
post_tagging_1, post_tagging_2 = Tagging.create!(taggable: post), Tagging.create!(taggable: post)
106+
author = Author.new(name: "Josh")
107+
author.posts = [post]
108+
categorization = Categorization.new
109+
categorization.author = author
110+
111+
assert_predicate post_tagging_1, :persisted?
112+
assert_predicate post_tagging_2, :persisted?
113+
assert_predicate post, :persisted?
114+
assert_predicate categorization, :new_record?
115+
assert_predicate categorization.author, :new_record?
116+
assert_queries_count(1) { assert_equal [post_tagging_1, post_tagging_2].sort, categorization.post_taggings.sort }
117+
end
118+
119+
def test_setting_nested_has_many_through_many_association_on_new_record_sets_nested_through_records
75120
account_1 = Account.create!(firm_name: "account 1", credit_limit: 100)
76121
subscriber_1 = Subscriber.create!(nick: "nick 1", account: account_1)
77122
account_2 = Account.create!(firm_name: "account 2", credit_limit: 100)
@@ -83,9 +128,11 @@ def test_setting_association_on_new_record_sets_nested_through_records
83128

84129
assert_predicate subscriber_1, :persisted?
85130
assert_predicate subscriber_2, :persisted?
131+
assert_predicate account_1, :persisted?
132+
assert_predicate account_2, :persisted?
86133
assert_predicate book, :new_record?
87134
book.subscriptions.each { |subscription| assert_predicate subscription, :new_record? }
88-
assert_equal [account_1, account_2].sort, book.subscriber_accounts.sort
135+
assert_no_queries { assert_equal [account_1, account_2].sort, book.subscriber_accounts.sort }
89136
end
90137

91138
def test_has_many_through_create_record

activerecord/test/cases/associations/has_one_through_associations_test.rb

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
require "models/shop_account"
2424
require "models/customer_carrier"
2525
require "models/cpk"
26+
require "models/rating"
2627

2728
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
2829
fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans,
@@ -52,7 +53,7 @@ def test_setting_association_on_new_record_sets_through_record
5253
assert_predicate club, :persisted?
5354
assert_predicate member, :new_record?
5455
assert_predicate member.current_membership, :new_record?
55-
assert_equal club, member.club
56+
assert_no_queries { assert_equal club, member.club }
5657
end
5758

5859
def test_setting_association_on_new_record_sets_nested_through_record
@@ -66,7 +67,38 @@ def test_setting_association_on_new_record_sets_nested_through_record
6667
assert_predicate club, :persisted?
6768
assert_predicate member, :new_record?
6869
assert_predicate member.current_membership, :new_record?
69-
assert_equal club.category, member.club_category
70+
assert_no_queries { assert_equal club.category, member.club_category }
71+
end
72+
73+
def test_setting_association_on_new_record_sets_not_loaded_nested_through_record
74+
category = Category.create!(name: "General")
75+
club = Club.create!(category: category)
76+
club.reload
77+
membership = CurrentMembership.new(club: club)
78+
member = Member.new
79+
member.current_membership = membership
80+
81+
assert_predicate category, :persisted?
82+
assert_predicate club, :persisted?
83+
assert_predicate member, :new_record?
84+
assert_predicate member.current_membership, :new_record?
85+
assert_queries_count(1) { assert_equal club.category, member.club_category }
86+
end
87+
88+
def test_setting_association_on_new_record_sets_nested_through_record_with_belongs_to
89+
essay = Essay.create!
90+
author = Author.create!(name: "Josh", owned_essay: essay)
91+
post = Post.create!(title: "Foo", body: "Bar", author: author)
92+
comment = Comment.new(post: post)
93+
rating = Rating.new
94+
rating.comment = comment
95+
96+
assert_predicate essay, :persisted?
97+
assert_predicate author, :persisted?
98+
assert_predicate post, :persisted?
99+
assert_predicate rating, :new_record?
100+
assert_predicate rating.comment, :new_record?
101+
assert_no_queries { assert_equal essay, rating.owned_essay }
70102
end
71103

72104
def test_creating_association_creates_through_record
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# frozen_string_literal: true
22

33
class FamilyTree < ActiveRecord::Base
4+
belongs_to :user
5+
46
belongs_to :member, class_name: "User", foreign_key: "member_id"
57
belongs_to :family
68
end

activerecord/test/models/member_detail.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class MemberDetail < ActiveRecord::Base
55
belongs_to :organization
66
has_one :member_type, through: :member
77
has_one :membership, through: :member
8+
has_one :sponsor, through: :membership
89
has_one :admittable, through: :member, source_type: "Member"
910

1011
has_many :organization_member_details, through: :organization, source: :member_details

activerecord/test/models/membership.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ class Membership < ActiveRecord::Base
44
enum :type, %i(Membership CurrentMembership SuperMembership SelectedMembership TenantMembership)
55
belongs_to :member
66
belongs_to :club
7+
has_one :sponsor, through: :club
78
end
89

910
class CurrentMembership < Membership

activerecord/test/models/post.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def greeting
4646
}
4747

4848
belongs_to :author
49+
has_one :owned_essay, through: :author
4950
belongs_to :readonly_author, -> { readonly }, class_name: "Author", foreign_key: :author_id
5051

5152
belongs_to :author_with_posts, -> { includes(:posts) }, class_name: "Author", foreign_key: :author_id

activerecord/test/models/rating.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
class Rating < ActiveRecord::Base
44
belongs_to :comment
5+
has_one :post, through: :comment
6+
has_one :owned_essay, through: :post
57
has_many :taggings, as: :taggable
68
has_many :taggings_without_tag, -> { left_joins(:tag).where("tags.id": [nil, 0]) }, as: :taggable, class_name: "Tagging"
79
has_many :taggings_with_no_tag, -> { joins("LEFT OUTER JOIN tags ON tags.id = taggings.tag_id").where("tags.id": nil) }, as: :taggable, class_name: "Tagging"

0 commit comments

Comments
 (0)