Skip to content

Commit a94d02f

Browse files
committed
Allow association foreign_key to be an Array
Currently the only way to setup a composite foreign key is to use `query_constraints`. With our intention to decouple `query_constraints` from `foreign_key` we need to allow `foreign_key` to be an Array to preserve the current behavior achieved with `query_constraints`.
1 parent a534bac commit a94d02f

File tree

7 files changed

+51
-72
lines changed

7 files changed

+51
-72
lines changed

activerecord/lib/active_record/reflection.rb

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,6 @@ def initialize(name, scope, options, active_record)
382382
@klass = options[:anonymous_class]
383383
@plural_name = active_record.pluralize_table_names ?
384384
name.to_s.pluralize : name.to_s
385-
validate_reflection!
386385
end
387386

388387
def autosave=(autosave)
@@ -434,17 +433,6 @@ def scope_for(relation, owner = nil)
434433
def derive_class_name
435434
name.to_s.camelize
436435
end
437-
438-
def validate_reflection!
439-
return unless options[:foreign_key].is_a?(Array)
440-
441-
message = <<~MSG.squish
442-
Passing #{options[:foreign_key]} array to :foreign_key option
443-
on the #{active_record}##{name} association is not supported.
444-
Use the query_constraints: #{options[:foreign_key]} option instead to represent a composite foreign key.
445-
MSG
446-
raise ArgumentError, message
447-
end
448436
end
449437

450438
# Holds all the metadata about an aggregation as it was specified in the
@@ -511,10 +499,14 @@ def join_table
511499
end
512500

513501
def foreign_key(infer_from_inverse_of: true)
514-
@foreign_key ||= if options[:query_constraints]
502+
@foreign_key ||= if options[:foreign_key]
503+
if options[:foreign_key].is_a?(Array)
504+
options[:foreign_key].map { |fk| fk.to_s.freeze }.freeze
505+
else
506+
options[:foreign_key].to_s.freeze
507+
end
508+
elsif options[:query_constraints]
515509
options[:query_constraints].map { |fk| fk.to_s.freeze }.freeze
516-
elsif options[:foreign_key]
517-
options[:foreign_key].to_s
518510
else
519511
derived_fk = derive_foreign_key(infer_from_inverse_of: infer_from_inverse_of)
520512

activerecord/test/cases/associations_test.rb

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -196,30 +196,6 @@ def test_querying_by_relation_with_composite_key
196196
assert_equal(expected_posts.map(&:id).sort, blog_posts.map(&:id).sort)
197197
end
198198

199-
def test_has_many_with_foreign_key_as_an_array_raises
200-
expected_message = <<~MSG.squish
201-
Passing [:blog_id, :blog_post_id] array to :foreign_key option
202-
on the Sharded::BlogPost#broken_array_fk_comments association is not supported.
203-
Use the query_constraints: [:blog_id, :blog_post_id] option instead to represent a composite foreign key.
204-
MSG
205-
assert_raises ArgumentError, match: expected_message do
206-
Sharded::BlogPost.has_many :broken_array_fk_comments,
207-
class_name: "Sharded::Comment", foreign_key: [:blog_id, :blog_post_id]
208-
end
209-
end
210-
211-
def test_belongs_to_with_foreign_key_as_an_array_raises
212-
expected_message = <<~MSG.squish
213-
Passing [:blog_id, :blog_post_id] array to :foreign_key option
214-
on the Sharded::Comment#broken_array_fk_blog_post association is not supported.
215-
Use the query_constraints: [:blog_id, :blog_post_id] option instead to represent a composite foreign key.
216-
MSG
217-
assert_raises ArgumentError, match: expected_message do
218-
Sharded::Comment.belongs_to :broken_array_fk_blog_post,
219-
class_name: "Sharded::Blog", foreign_key: [:blog_id, :blog_post_id]
220-
end
221-
end
222-
223199
def test_has_many_association_with_composite_foreign_key_loads_records
224200
blog_post = sharded_blog_posts(:great_post_blog_one)
225201

@@ -228,6 +204,20 @@ def test_has_many_association_with_composite_foreign_key_loads_records
228204
assert_includes(comments, sharded_comments(:great_comment_blog_post_one))
229205
end
230206

207+
def test_belongs_to_with_explicit_composite_foreign_key
208+
car = Cpk::Car.create(make: "Tesla", model: "Model S")
209+
review = Cpk::CarReview.create(car: car, comment: "Great car!", rating: 5)
210+
211+
review.reload
212+
213+
sql = capture_sql do
214+
assert_equal(car, review.car)
215+
end
216+
217+
assert_match(/#{Regexp.escape(Cpk::Car.lease_connection.quote_table_name("cpk_cars.make"))} =/, sql.first)
218+
assert_match(/#{Regexp.escape(Cpk::Car.lease_connection.quote_table_name("cpk_cars.model"))} =/, sql.first)
219+
end
220+
231221
def test_cpk_model_has_many_records_by_id_attribute
232222
order = cpk_orders(:cpk_groceries_order_1)
233223
_order_shop_id, order_id = order.id

activerecord/test/cases/reflection_test.rb

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -226,17 +226,6 @@ def test_has_many_reflection
226226
assert_equal "companies", Firm.reflect_on_association(:clients_of_firm).table_name
227227
end
228228

229-
def test_has_many_reflection_with_array_fk_raises
230-
expected_message = <<~MSG.squish
231-
Passing [:firm_id, :firm_name] array to :foreign_key option
232-
on the Firm#clients association is not supported.
233-
Use the query_constraints: [:firm_id, :firm_name] option instead to represent a composite foreign key.
234-
MSG
235-
assert_raises ArgumentError, match: expected_message do
236-
ActiveRecord::Reflection.create(:has_many, :clients, nil, { foreign_key: [:firm_id, :firm_name] }, Firm)
237-
end
238-
end
239-
240229
def test_has_one_reflection
241230
reflection_for_account = ActiveRecord::Reflection.create(:has_one, :account, nil, { foreign_key: "firm_id", dependent: :destroy }, Firm)
242231
assert_equal reflection_for_account, Firm.reflect_on_association(:account)
@@ -245,17 +234,6 @@ def test_has_one_reflection
245234
assert_equal "accounts", Firm.reflect_on_association(:account).table_name
246235
end
247236

248-
def has_one_reflection_with_array_fk_raises
249-
expected_message = <<~MSG.squish
250-
Passing [:firm_id, :firm_name] array to :foreign_key option
251-
on the Firm#account association is not supported.
252-
Use the query_constraints: [:firm_id, :firm_name] option instead to represent a composite foreign key.
253-
MSG
254-
assert_raises ArgumentError, match: expected_message do
255-
ActiveRecord::Reflection.create(:has_one, :account, nil, { foreign_key: [:firm_id, :firm_name] }, Firm)
256-
end
257-
end
258-
259237
def test_belongs_to_inferred_foreign_key_from_assoc_name
260238
Company.belongs_to :foo
261239
assert_equal "foo_id", Company.reflect_on_association(:foo).foreign_key
@@ -265,17 +243,6 @@ def test_belongs_to_inferred_foreign_key_from_assoc_name
265243
assert_equal "xyzzy_id", Company.reflect_on_association(:baz).foreign_key
266244
end
267245

268-
def test_belongs_to_reflection_with_array_fk_raises
269-
expected_message = <<~MSG.squish
270-
Passing [:firm_id, :firm_name] array to :foreign_key option
271-
on the Firm#client association is not supported.
272-
Use the query_constraints: [:firm_id, :firm_name] option instead to represent a composite foreign key.
273-
MSG
274-
assert_raises ArgumentError, match: expected_message do
275-
ActiveRecord::Reflection.create(:belongs_to, :client, nil, { foreign_key: [:firm_id, :firm_name] }, Firm)
276-
end
277-
end
278-
279246
def test_association_reflection_in_modules
280247
ActiveRecord::Base.store_full_sti_class = false
281248

activerecord/test/models/cpk.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@
1010
require_relative "cpk/chapter"
1111
require_relative "cpk/post"
1212
require_relative "cpk/comment"
13+
require_relative "cpk/car"
14+
require_relative "cpk/car_review"

activerecord/test/models/cpk/car.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module Cpk
4+
class Car < ActiveRecord::Base
5+
self.table_name = :cpk_cars
6+
end
7+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
module Cpk
4+
class CarReview < ActiveRecord::Base
5+
self.table_name = :cpk_car_reviews
6+
7+
belongs_to :car, foreign_key: [:car_make, :car_model]
8+
end
9+
end

activerecord/test/schema/schema.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,18 @@
307307
t.index :order_id
308308
end
309309

310+
create_table :cpk_cars, force: true, primary_key: [:make, :model] do |t|
311+
t.string :make, null: false
312+
t.string :model, null: false
313+
end
314+
315+
create_table :cpk_car_reviews, force: true do |t|
316+
t.string :car_make, null: false
317+
t.string :car_model, null: false
318+
t.text :comment
319+
t.integer :rating
320+
end
321+
310322
create_table :paragraphs, force: true do |t|
311323
t.references :book
312324
end

0 commit comments

Comments
 (0)