Skip to content

Commit fb127c0

Browse files
committed
Define ActiveRecord::Base#id API for composite primary key models
Given a model with a composite primary key: ```ruby class Order < ActiveRecord::Base self.primary_key = [:shop_id, :id] end ``` `ActiveRecord::Base#id` method will return an array of values for every column of the primary key. ```ruby order = Order.create!(shop_id: 1, id: 2) order.id # => [1, 2] ``` The `id` column is accessible through the `read_attribute` method: ```ruby order.read_attribute(:id) # => 2 ```
1 parent 3dd5d49 commit fb127c0

File tree

10 files changed

+101
-4
lines changed

10 files changed

+101
-4
lines changed

activerecord/lib/active_record/attribute_methods/primary_key.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ def to_key
1616

1717
# Returns the primary key column's value.
1818
def id
19-
_read_attribute(@primary_key)
19+
return _read_attribute(@primary_key) unless @primary_key.is_a?(Array)
20+
21+
@primary_key.map { |pk| _read_attribute(pk) }
2022
end
2123

2224
# Sets the primary key column's value.
@@ -120,12 +122,20 @@ def get_primary_key(base_name) # :nodoc:
120122
#
121123
# Project.primary_key # => "foo_id"
122124
def primary_key=(value)
123-
@primary_key = value && -value.to_s
125+
@primary_key = derive_primary_key(value)
124126
@quoted_primary_key = nil
125127
@attributes_builder = nil
126128
end
127129

128130
private
131+
def derive_primary_key(value)
132+
return unless value
133+
134+
return -value.to_s unless value.is_a?(Array)
135+
136+
value.map { |v| -v.to_s }.freeze
137+
end
138+
129139
def inherited(base)
130140
super
131141
base.class_eval do

activerecord/lib/active_record/attribute_methods/read.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def read_attribute(attr_name, &block)
2929
name = attr_name.to_s
3030
name = self.class.attribute_aliases[name] || name
3131

32-
name = @primary_key if name == "id" && @primary_key
32+
name = @primary_key if name == "id" && @primary_key && !@primary_key.is_a?(Array)
3333
@attributes.fetch_value(name, &block)
3434
end
3535

activerecord/lib/active_record/fixtures.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,8 @@ def [](key)
769769
def find
770770
raise FixtureClassNotFound, "No class attached to find." unless model_class
771771
object = model_class.unscoped do
772-
model_class.find(fixture[model_class.primary_key])
772+
pk_clauses = fixture.slice(*Array(model_class.primary_key))
773+
model_class.find_by!(pk_clauses)
773774
end
774775
# Fixtures can't be eagerly loaded
775776
object.instance_variable_set(:@strict_loading, false)

activerecord/test/cases/primary_keys_test.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require "models/mixed_case_monkey"
1111
require "models/dashboard"
1212
require "models/non_primary_key"
13+
require "models/cpk"
1314

1415
class PrimaryKeysTest < ActiveRecord::TestCase
1516
fixtures :topics, :subscribers, :movies, :mixed_case_monkeys
@@ -331,6 +332,8 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase
331332

332333
self.use_transactional_tests = false
333334

335+
fixtures :cpk_books, :cpk_orders
336+
334337
def setup
335338
@connection = ActiveRecord::Base.connection
336339
@connection.schema_cache.clear!
@@ -387,6 +390,19 @@ def test_dumping_composite_primary_key_out_of_order
387390
schema = dump_table_schema "barcodes_reverse"
388391
assert_match %r{create_table "barcodes_reverse", primary_key: \["code", "region"\]}, schema
389392
end
393+
394+
def test_model_with_a_composite_primary_key
395+
assert_equal(["author_id", "number"], Cpk::Book.primary_key)
396+
assert_equal(["shop_id", "id"], Cpk::Order.primary_key)
397+
end
398+
399+
def test_id_is_not_defined_on_a_model_with_composite_primary_key
400+
book = cpk_books(:cpk_great_author_first_book)
401+
order = cpk_orders(:cpk_groceries_order_1)
402+
403+
assert_equal([book.author_id, book.number], book.id)
404+
assert_equal([order.shop_id, order.read_attribute(:id)], order.id)
405+
end
390406
end
391407

392408
class PrimaryKeyIntegerNilDefaultTest < ActiveRecord::TestCase
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
_fixture:
2+
model_class: Cpk::Book
3+
4+
cpk_great_author_first_book:
5+
author_id: 1
6+
number: 1
7+
title: "The first book"
8+
revision: 1
9+
10+
cpk_great_author_second_book:
11+
author_id: 1
12+
number: 2
13+
title: "The second book"
14+
revision: 1
15+
16+
cpk_famous_author_first_book:
17+
author_id: 2
18+
number: 1
19+
title: "Ruby on Rails"
20+
revision: 1
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
_fixture:
2+
model_class: Cpk::Order
3+
4+
cpk_groceries_order_1:
5+
id: 1
6+
shop_id: 1
7+
status: "paid"
8+
9+
cpk_groceries_order_2:
10+
id: 2
11+
shop_id: 1
12+
status: "paid"
13+
14+
cpk_coffee_order_1:
15+
id: 3
16+
shop_id: 2
17+
status: "cancelled"

activerecord/test/models/cpk.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "cpk/book"
4+
require_relative "cpk/order"

activerecord/test/models/cpk/book.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
module Cpk
4+
class Book < ActiveRecord::Base
5+
self.table_name = :cpk_books
6+
self.primary_key = [:author_id, :number]
7+
end
8+
end

activerecord/test/models/cpk/order.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
module Cpk
4+
class Order < ActiveRecord::Base
5+
self.table_name = :cpk_orders
6+
self.primary_key = [:shop_id, :id]
7+
end
8+
end

activerecord/test/schema/schema.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,19 @@
239239
t.references :citation
240240
end
241241

242+
create_table :cpk_books, primary_key: [:author_id, :number], force: true do |t|
243+
t.integer :author_id
244+
t.integer :number
245+
t.string :title
246+
t.integer :revision
247+
end
248+
249+
create_table :cpk_orders, primary_key: [:shop_id, :id], force: true do |t|
250+
t.integer :shop_id
251+
t.integer :id
252+
t.string :status
253+
end
254+
242255
create_table :clothing_items, force: true do |t|
243256
t.string :clothing_type
244257
t.string :color

0 commit comments

Comments
 (0)