Skip to content

Commit 4199eeb

Browse files
Add regression test for has_many_through associations failing to create join models
This commit adds a test that demonstrates a regression in has_many_through associations where multiple has_many_through associations associate the same parent with the same child relation. The regression is ultimately caused by rails/rails@0d38f2a, which avoids saving a through association target if we expect the target to be persisted by another (direct) association. The problem is that if we have multiple has_many_through associations that associate the same parent with the same child, we will autosave the first set of associations from the parent, but avoid saving the through target a second time (once it's persisted by the first association), and fail to create the join model for the second association chain.
1 parent b680ec7 commit 4199eeb

File tree

8 files changed

+67
-0
lines changed

8 files changed

+67
-0
lines changed

activerecord/test/cases/associations/has_many_through_associations_test.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
require "models/member"
3030
require "models/membership"
3131
require "models/club"
32+
require "models/program"
33+
require "models/program_offering"
34+
require "models/enrollment"
3235
require "models/organization"
3336
require "models/user"
3437
require "models/family"
@@ -1548,6 +1551,21 @@ def test_has_many_through_update_ids_with_conditions
15481551
assert_equal [], author.nonspecial_categories_with_condition_ids
15491552
end
15501553

1554+
def test_has_many_through_from_same_parent_to_same_child_creates_join_models
1555+
club = Club.new(name: "Awesome Rails Club")
1556+
member = club.simple_members.build(name: "Jane Doe")
1557+
1558+
program = Program.new(name: "Learn Ruby on Rails")
1559+
program.members << member
1560+
1561+
club.programs << program
1562+
1563+
club.save!
1564+
1565+
assert_equal(1, program.enrollments.size)
1566+
assert_equal(1, club.simple_memberships.size)
1567+
end
1568+
15511569
def test_single_has_many_through_association_with_unpersisted_parent_instance
15521570
post_with_single_has_many_through = Class.new(Post) do
15531571
def self.name; "PostWithSingleHasManyThrough"; end

activerecord/test/models/club.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ class Club < ActiveRecord::Base
1515

1616
scope :general, -> { left_joins(:category).where(categories: { name: "General" }).unscope(:limit) }
1717

18+
has_many :program_offerings
19+
has_many :programs, through: :program_offerings
20+
21+
has_many :simple_memberships, class_name: "Membership"
22+
has_many :simple_members, through: :simple_memberships, foreign_key: "member_id"
23+
1824
accepts_nested_attributes_for :membership
1925

2026
private
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
class Enrollment < ActiveRecord::Base
4+
belongs_to :program
5+
belongs_to :member, class_name: "SimpleMember"
6+
end

activerecord/test/models/member.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ class Member < ActiveRecord::Base
4444
scope :with_member_type_id, -> (id) { where(member_type_id: id) }
4545
end
4646

47+
class SimpleMember < ActiveRecord::Base
48+
self.table_name = "members"
49+
50+
has_many :memberships
51+
has_many :clubs, through: :memberships
52+
end
53+
4754
class SelfMember < ActiveRecord::Base
4855
self.table_name = "members"
4956
has_and_belongs_to_many :friends, class_name: "SelfMember", join_table: "member_friends"

activerecord/test/models/membership.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ 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
8+
9+
belongs_to :simple_member, foreign_key: "member_id"
710
end
811

912
class CurrentMembership < Membership

activerecord/test/models/program.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
class Program < ActiveRecord::Base
4+
has_many :enrollments
5+
has_many :members, through: :enrollments, class_name: "SimpleMember"
6+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
class ProgramOffering < ActiveRecord::Base
4+
belongs_to :club
5+
belongs_to :program
6+
end

activerecord/test/schema/schema.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,11 @@
588588
t.references :car, index: false
589589
end
590590

591+
create_table :enrollments, force: true do |t|
592+
t.integer :program_id
593+
t.integer :member_id
594+
end
595+
591596
create_table :entrants, force: true do |t|
592597
t.string :name, null: false
593598
t.integer :course_id, null: false
@@ -1018,6 +1023,16 @@
10181023
t.decimal :discounted_price
10191024
end
10201025

1026+
create_table :program_offerings, force: true do |t|
1027+
t.integer :club_id
1028+
t.integer :program_id
1029+
t.datetime :start_date
1030+
end
1031+
1032+
create_table :programs, force: true do |t|
1033+
t.string :name
1034+
end
1035+
10211036
add_check_constraint :products, "price > discounted_price", name: "products_price_check"
10221037

10231038
create_table :product_types, force: true do |t|

0 commit comments

Comments
 (0)