Skip to content

Commit 411bfa2

Browse files
authored
Merge pull request rails#49088 from eileencodes/derive-foreign-key-query-constraints
Allow Active Record to derive the association query constraints
2 parents 0e10f7c + a332dd9 commit 411bfa2

File tree

7 files changed

+90
-9
lines changed

7 files changed

+90
-9
lines changed

activerecord/lib/active_record/autosave_association.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,8 @@ def compute_primary_key(reflection, record)
532532
primary_key_options
533533
elsif reflection.options[:query_constraints] && (query_constraints = record.class.query_constraints_list)
534534
query_constraints
535+
elsif record.class.has_query_constraints? && !reflection.options[:foreign_key]
536+
record.class.query_constraints_list
535537
else
536538
record.class.primary_key
537539
end

activerecord/lib/active_record/reflection.rb

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,8 +502,16 @@ def foreign_key(infer_from_inverse_of: true)
502502
@foreign_key ||= if options[:query_constraints]
503503
# composite foreign keys support
504504
options[:query_constraints].map { |fk| fk.to_s.freeze }.freeze
505+
elsif options[:foreign_key]
506+
options[:foreign_key].to_s
505507
else
506-
-(options[:foreign_key]&.to_s || derive_foreign_key(infer_from_inverse_of: infer_from_inverse_of))
508+
derived_fk = derive_foreign_key(infer_from_inverse_of: infer_from_inverse_of)
509+
510+
if active_record.has_query_constraints?
511+
derived_fk = derive_fk_query_constraints(active_record, derived_fk)
512+
end
513+
514+
derived_fk
507515
end
508516
end
509517

@@ -759,6 +767,50 @@ def derive_foreign_key(infer_from_inverse_of: true)
759767
end
760768
end
761769

770+
def derive_fk_query_constraints(klass, foreign_key)
771+
primary_query_constraints = klass.query_constraints_list
772+
owner_pk = klass.primary_key
773+
774+
if primary_query_constraints.size != 2
775+
raise ArgumentError, <<~MSG.squish
776+
The query constraints list on the `#{klass}` model has more than 2
777+
attributes. Active Record is unable to derive the query constraints
778+
for the association. You need to explicitly define the query constraints
779+
for this association.
780+
MSG
781+
end
782+
783+
if !primary_query_constraints.include?(owner_pk)
784+
raise ArgumentError, <<~MSG.squish
785+
The query constraints on the `#{klass}` model does not include the primary
786+
key so Active Record is unable to derive the foreign key constraints for
787+
the association. You need to explicitly define the query constraints for this
788+
association.
789+
MSG
790+
end
791+
792+
# The primary key and foreign key are both already in the query constraints
793+
# so we don't want to derive the key. In this case we want a single key.
794+
if primary_query_constraints.include?(owner_pk) && primary_query_constraints.include?(foreign_key)
795+
return foreign_key
796+
end
797+
798+
first_key, last_key = primary_query_constraints
799+
800+
if first_key == owner_pk
801+
[foreign_key, last_key.to_s]
802+
elsif last_key == owner_pk
803+
[first_key.to_s, foreign_key]
804+
else
805+
raise ArgumentError, <<~MSG.squish
806+
Active Record couldn't correctly interpret the query constraints
807+
for the `#{klass}` model. The query constraints on `#{klass}` are
808+
`#{primary_query_constraints}` and the foreign key is `#{foreign_key}`.
809+
You need to explicitly set the query constraints for this association.
810+
MSG
811+
end
812+
end
813+
762814
def derive_join_table
763815
ModelSchema.derive_join_table_name active_record.table_name, klass.table_name
764816
end

activerecord/test/cases/associations_test.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class AssociationsTest < ActiveRecord::TestCase
4545
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
4646
:computers, :people, :readers, :authors, :author_addresses, :author_favorites,
4747
:comments, :posts, :sharded_blogs, :sharded_blog_posts, :sharded_comments, :sharded_tags, :sharded_blog_posts_tags,
48-
:cpk_orders
48+
:cpk_orders, :cpk_books, :cpk_reviews
4949

5050
def test_eager_loading_should_not_change_count_of_children
5151
liquid = Liquid.create(name: "salty")
@@ -225,6 +225,18 @@ def test_has_many_association_from_a_model_with_query_constraints_different_from
225225
assert_equal(expected_comments.sort, comments.sort)
226226
end
227227

228+
def test_query_constraints_over_three_without_defining_explicit_foreign_key_query_constraints_raises
229+
Sharded::BlogPostWithRevision.has_many :comments_without_query_constraints, primary_key: [:blog_id, :id], class_name: "Comment"
230+
blog_post = sharded_blog_posts(:great_post_blog_one)
231+
blog_post = Sharded::BlogPostWithRevision.find(blog_post.id)
232+
233+
error = assert_raises ArgumentError do
234+
blog_post.comments_without_query_constraints.to_a
235+
end
236+
237+
assert_equal "The query constraints list on the `Sharded::BlogPostWithRevision` model has more than 2 attributes. Active Record is unable to derive the query constraints for the association. You need to explicitly define the query constraints for this association.", error.message
238+
end
239+
228240
def test_model_with_composite_query_constraints_has_many_association_sql
229241
blog_post = sharded_blog_posts(:great_post_blog_one)
230242

@@ -315,6 +327,20 @@ def test_assign_composite_foreign_key_belongs_to_association
315327
assert_equal(another_blog.id, comment.blog_id)
316328
end
317329

330+
def test_query_constraints_that_dont_include_the_primary_key_raise
331+
original = Sharded::BlogPost.instance_variable_get(:@query_constraints_list)
332+
Sharded::BlogPost.query_constraints :title, :revision
333+
Sharded::BlogPost.has_many :comments_without_query_constraints, primary_key: [:blog_id, :id], class_name: "Comment"
334+
blog_post = sharded_blog_posts(:great_post_blog_one)
335+
336+
error = assert_raises ArgumentError do
337+
blog_post.comments_without_query_constraints.to_a
338+
end
339+
340+
assert_equal "The query constraints on the `Sharded::BlogPost` model does not include the primary key so Active Record is unable to derive the foreign key constraints for the association. You need to explicitly define the query constraints for this association.", error.message
341+
ensure
342+
Sharded::BlogPost.instance_variable_set(:@query_constraints_list, original)
343+
end
318344

319345
def test_assign_belongs_to_cpk_model_by_id_attribute
320346
order = cpk_orders(:cpk_groceries_order_1)

activerecord/test/models/sharded/blog_post.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ class BlogPost < ActiveRecord::Base
66
query_constraints :blog_id, :id
77

88
belongs_to :blog
9-
has_many :comments, query_constraints: [:blog_id, :blog_post_id]
10-
has_many :delete_comments, class_name: "Sharded::Comment", query_constraints: [:blog_id, :blog_post_id], dependent: :delete_all
9+
has_many :comments
10+
has_many :delete_comments, class_name: "Sharded::Comment", dependent: :delete_all
1111

12-
has_many :blog_post_tags, query_constraints: [:blog_id, :blog_post_id]
12+
has_many :blog_post_tags
1313
has_many :tags, through: :blog_post_tags
1414
end
1515
end

activerecord/test/models/sharded/blog_post_tag.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
module Sharded
44
class BlogPostTag < ActiveRecord::Base
55
self.table_name = :sharded_blog_posts_tags
6+
query_constraints :blog_id, :id
67

7-
belongs_to :blog_post, query_constraints: [:blog_id, :blog_post_id]
8-
belongs_to :tag, query_constraints: [:blog_id, :tag_id]
8+
belongs_to :blog_post
9+
belongs_to :tag
910
end
1011
end

activerecord/test/models/sharded/comment.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class Comment < ActiveRecord::Base
55
self.table_name = :sharded_comments
66
query_constraints :blog_id, :id
77

8-
belongs_to :blog_post, query_constraints: [:blog_id, :blog_post_id]
8+
belongs_to :blog_post
99
belongs_to :blog_post_by_id, class_name: "Sharded::BlogPost", foreign_key: :blog_post_id
1010
belongs_to :blog
1111
end

activerecord/test/models/sharded/tag.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class Tag < ActiveRecord::Base
55
self.table_name = :sharded_tags
66
query_constraints :blog_id, :id
77

8-
has_many :blog_post_tags, query_constraints: [:blog_id, :tag_id]
8+
has_many :blog_post_tags
99
has_many :blog_posts, through: :blog_post_tags
1010
end
1111
end

0 commit comments

Comments
 (0)