Skip to content

Commit b3ec7b2

Browse files
szimekjeremy
authored andcommitted
Add primary_key option to belongs_to association
[rails#765 state:committed] Signed-off-by: Jeremy Kemper <[email protected]>
1 parent ae85927 commit b3ec7b2

File tree

14 files changed

+163
-20
lines changed

14 files changed

+163
-20
lines changed

activerecord/CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
*2.3.3 (July 12, 2009)*
22

3+
* Added :primary_key option to belongs_to associations. #765 [Szymon Nowak, Philip Hallstrom, Noel Rocha]
4+
# employees.company_name references companies.name
5+
Employee.belongs_to :company, :primary_key => 'name', :foreign_key => 'company_name'
6+
37
* Added :touch option to belongs_to associations that will touch the parent record when the current record is saved or destroyed [DHH]
48

59
* Added ActiveRecord::Base#touch to update the updated_at/on attributes (or another specified timestamp) with the current time [DHH]

activerecord/lib/active_record/associations.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,8 @@ def has_one(association_id, options = {})
957957
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use
958958
# "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt>
959959
# will use a foreign key of "favorite_person_id".
960+
# [:primary_key]
961+
# Specify the method that returns the primary key of associated object used for the association. By default this is id.
960962
# [:dependent]
961963
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
962964
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when
@@ -987,6 +989,7 @@ def has_one(association_id, options = {})
987989
#
988990
# Option examples:
989991
# belongs_to :firm, :foreign_key => "client_of"
992+
# belongs_to :person, :primary_key => "name", :foreign_key => "person_name"
990993
# belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
991994
# belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
992995
# :conditions => 'discounts > #{payments_count}'
@@ -1320,14 +1323,14 @@ def add_counter_cache_callbacks(reflection)
13201323
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
13211324
define_method(method_name) do
13221325
association = send(reflection.name)
1323-
association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
1326+
association.class.increment_counter(cache_column, association.id) unless association.nil?
13241327
end
13251328
after_create(method_name)
13261329

13271330
method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
13281331
define_method(method_name) do
13291332
association = send(reflection.name)
1330-
association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
1333+
association.class.decrement_counter(cache_column, association.id) unless association.nil?
13311334
end
13321335
before_destroy(method_name)
13331336

@@ -1519,7 +1522,7 @@ def create_has_one_through_reflection(association_id, options)
15191522

15201523
mattr_accessor :valid_keys_for_belongs_to_association
15211524
@@valid_keys_for_belongs_to_association = [
1522-
:class_name, :foreign_key, :foreign_type, :remote, :select, :conditions,
1525+
:class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions,
15231526
:include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
15241527
:validate, :touch
15251528
]

activerecord/lib/active_record/associations/belongs_to_association.rb

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def replace(record)
1414

1515
if record.nil?
1616
if counter_cache_name && !@owner.new_record?
17-
@reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
17+
@reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name]
1818
end
1919

2020
@target = @owner[@reflection.primary_key_name] = nil
@@ -27,7 +27,7 @@ def replace(record)
2727
end
2828

2929
@target = (AssociationProxy === record ? record.target : record)
30-
@owner[@reflection.primary_key_name] = record.id unless record.new_record?
30+
@owner[@reflection.primary_key_name] = record_id(record) unless record.new_record?
3131
@updated = true
3232
end
3333

@@ -41,18 +41,36 @@ def updated?
4141

4242
private
4343
def find_target
44-
@reflection.klass.find(
44+
find_method = if @reflection.options[:primary_key]
45+
"find_by_#{@reflection.options[:primary_key]}"
46+
else
47+
"find"
48+
end
49+
@reflection.klass.send(find_method,
4550
@owner[@reflection.primary_key_name],
4651
:select => @reflection.options[:select],
4752
:conditions => conditions,
4853
:include => @reflection.options[:include],
4954
:readonly => @reflection.options[:readonly]
50-
)
55+
) if @owner[@reflection.primary_key_name]
5156
end
5257

5358
def foreign_key_present
5459
!@owner[@reflection.primary_key_name].nil?
5560
end
61+
62+
def record_id(record)
63+
record.send(@reflection.options[:primary_key] || :id)
64+
end
65+
66+
def previous_record_id
67+
@previous_record_id ||= if @reflection.options[:primary_key]
68+
previous_record = @owner.send(@reflection.name)
69+
previous_record.nil? ? nil : previous_record.id
70+
else
71+
@owner[@reflection.primary_key_name]
72+
end
73+
end
5674
end
5775
end
5876
end

activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def replace(record)
77
else
88
@target = (AssociationProxy === record ? record.target : record)
99

10-
@owner[@reflection.primary_key_name] = record.id
10+
@owner[@reflection.primary_key_name] = record_id(record)
1111
@owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s
1212

1313
@updated = true
@@ -41,6 +41,10 @@ def foreign_key_present
4141
!@owner[@reflection.primary_key_name].nil?
4242
end
4343

44+
def record_id(record)
45+
record.send(@reflection.options[:primary_key] || :id)
46+
end
47+
4448
def association_class
4549
@owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
4650
end

activerecord/lib/active_record/autosave_association.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,8 @@ def save_belongs_to_association(reflection)
340340
association.save(!autosave) if association.new_record? || autosave
341341

342342
if association.updated?
343-
self[reflection.primary_key_name] = association.id
343+
association_id = association.send(reflection.options[:primary_key] || :id)
344+
self[reflection.primary_key_name] = association_id
344345
# TODO: Removing this code doesn't seem to matter…
345346
if reflection.options[:polymorphic]
346347
self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s

activerecord/test/cases/associations/belongs_to_associations_test.rb

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
require 'models/comment'
1515
require 'models/sponsor'
1616
require 'models/member'
17+
require 'models/essay'
1718

1819
class BelongsToAssociationsTest < ActiveRecord::TestCase
1920
fixtures :accounts, :companies, :developers, :projects, :topics,
@@ -25,6 +26,11 @@ def test_belongs_to
2526
assert !Client.find(3).firm.nil?, "Microsoft should have a firm"
2627
end
2728

29+
def test_belongs_to_with_primary_key
30+
client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name)
31+
assert_equal companies(:first_firm).name, client.firm_with_primary_key.name
32+
end
33+
2834
def test_proxy_assignment
2935
account = Account.find(1)
3036
assert_nothing_raised { account.firm = account.firm }
@@ -47,6 +53,13 @@ def test_natural_assignment
4753
assert_equal apple.id, citibank.firm_id
4854
end
4955

56+
def test_natural_assignment_with_primary_key
57+
apple = Firm.create("name" => "Apple")
58+
citibank = Client.create("name" => "Primary key client")
59+
citibank.firm_with_primary_key = apple
60+
assert_equal apple.name, citibank.firm_name
61+
end
62+
5063
def test_no_unexpected_aliasing
5164
first_firm = companies(:first_firm)
5265
another_firm = companies(:another_firm)
@@ -69,13 +82,29 @@ def test_creating_the_belonging_object
6982
assert_equal apple, citibank.firm
7083
end
7184

85+
def test_creating_the_belonging_object_with_primary_key
86+
client = Client.create(:name => "Primary key client")
87+
apple = client.create_firm_with_primary_key("name" => "Apple")
88+
assert_equal apple, client.firm_with_primary_key
89+
client.save
90+
client.reload
91+
assert_equal apple, client.firm_with_primary_key
92+
end
93+
7294
def test_building_the_belonging_object
7395
citibank = Account.create("credit_limit" => 10)
7496
apple = citibank.build_firm("name" => "Apple")
7597
citibank.save
7698
assert_equal apple.id, citibank.firm_id
7799
end
78100

101+
def test_building_the_belonging_object_with_primary_key
102+
client = Client.create(:name => "Primary key client")
103+
apple = client.build_firm_with_primary_key("name" => "Apple")
104+
client.save
105+
assert_equal apple.name, client.firm_name
106+
end
107+
79108
def test_natural_assignment_to_nil
80109
client = Client.find(3)
81110
client.firm = nil
@@ -84,6 +113,14 @@ def test_natural_assignment_to_nil
84113
assert_nil client.client_of
85114
end
86115

116+
def test_natural_assignment_to_nil_with_primary_key
117+
client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name)
118+
client.firm_with_primary_key = nil
119+
client.save
120+
assert_nil client.firm_with_primary_key(true)
121+
assert_nil client.client_of
122+
end
123+
87124
def test_with_different_class_name
88125
assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name
89126
assert_not_nil Company.find(3).firm_with_other_name, "Microsoft should have a firm"
@@ -110,6 +147,17 @@ def test_belongs_to_counter
110147
assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
111148
end
112149

150+
def test_belongs_to_with_primary_key_counter
151+
debate = Topic.create("title" => "debate")
152+
assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet"
153+
154+
trash = debate.replies_with_primary_key.create("title" => "blah!", "content" => "world around!")
155+
assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created"
156+
157+
trash.destroy
158+
assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
159+
end
160+
113161
def test_belongs_to_counter_with_assigning_nil
114162
p = Post.find(1)
115163
c = Comment.find(1)
@@ -122,6 +170,18 @@ def test_belongs_to_counter_with_assigning_nil
122170
assert_equal 1, Post.find(p.id).comments.size
123171
end
124172

173+
def test_belongs_to_with_primary_key_counter_with_assigning_nil
174+
debate = Topic.create("title" => "debate")
175+
reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate")
176+
177+
assert_equal debate.title, reply.parent_title
178+
assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count")
179+
180+
reply.topic_with_primary_key = nil
181+
182+
assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count")
183+
end
184+
125185
def test_belongs_to_counter_with_reassigning
126186
t1 = Topic.create("title" => "t1")
127187
t2 = Topic.create("title" => "t2")
@@ -219,6 +279,18 @@ def test_assignment_before_child_saved
219279
assert_equal firm, final_cut.firm(true)
220280
end
221281

282+
def test_assignment_before_child_saved_with_primary_key
283+
final_cut = Client.new("name" => "Final Cut")
284+
firm = Firm.find(1)
285+
final_cut.firm_with_primary_key = firm
286+
assert final_cut.new_record?
287+
assert final_cut.save
288+
assert !final_cut.new_record?
289+
assert !firm.new_record?
290+
assert_equal firm, final_cut.firm_with_primary_key
291+
assert_equal firm, final_cut.firm_with_primary_key(true)
292+
end
293+
222294
def test_new_record_with_foreign_key_but_no_object
223295
c = Client.new("firm_id" => 1)
224296
assert_equal Firm.find(:first), c.firm_with_basic_id
@@ -297,26 +369,52 @@ def test_polymorphic_assignment_foreign_type_field_updating
297369
member = Member.create
298370
sponsor.sponsorable = member
299371
assert_equal "Member", sponsor.sponsorable_type
300-
372+
301373
# should update when assigning a new record
302374
sponsor = Sponsor.new
303375
member = Member.new
304376
sponsor.sponsorable = member
305377
assert_equal "Member", sponsor.sponsorable_type
306378
end
307-
379+
380+
def test_polymorphic_assignment_with_primary_key_foreign_type_field_updating
381+
# should update when assigning a saved record
382+
essay = Essay.new
383+
writer = Author.create(:name => "David")
384+
essay.writer = writer
385+
assert_equal "Author", essay.writer_type
386+
387+
# should update when assigning a new record
388+
essay = Essay.new
389+
writer = Author.new
390+
essay.writer = writer
391+
assert_equal "Author", essay.writer_type
392+
end
393+
308394
def test_polymorphic_assignment_updates_foreign_id_field_for_new_and_saved_records
309395
sponsor = Sponsor.new
310396
saved_member = Member.create
311397
new_member = Member.new
312-
398+
313399
sponsor.sponsorable = saved_member
314400
assert_equal saved_member.id, sponsor.sponsorable_id
315-
401+
316402
sponsor.sponsorable = new_member
317403
assert_equal nil, sponsor.sponsorable_id
318404
end
319405

406+
def test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records
407+
essay = Essay.new
408+
saved_writer = Author.create(:name => "David")
409+
new_writer = Author.new
410+
411+
essay.writer = saved_writer
412+
assert_equal saved_writer.name, essay.writer_id
413+
414+
essay.writer = new_writer
415+
assert_equal nil, essay.writer_id
416+
end
417+
320418
def test_belongs_to_proxy_should_not_respond_to_private_methods
321419
assert_raise(NoMethodError) { companies(:first_firm).private_method }
322420
assert_raise(NoMethodError) { companies(:second_client).firm.private_method }

activerecord/test/cases/base_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2014,7 +2014,7 @@ def test_inspect_class
20142014

20152015
def test_inspect_instance
20162016
topic = topics(:first)
2017-
assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "[email protected]", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, type: nil>), topic.inspect
2017+
assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "[email protected]", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, parent_title: nil, type: nil>), topic.inspect
20182018
end
20192019

20202020
def test_inspect_new_instance

activerecord/test/cases/reflection_test.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,25 @@ def test_column_null_not_null
2121

2222
def test_read_attribute_names
2323
assert_equal(
24-
%w( id title author_name author_email_address bonus_time written_on last_read content approved replies_count parent_id type ).sort,
24+
%w( id title author_name author_email_address bonus_time written_on last_read content approved replies_count parent_id parent_title type ).sort,
2525
@first.attribute_names
2626
)
2727
end
2828

2929
def test_columns
30-
assert_equal 12, Topic.columns.length
30+
assert_equal 13, Topic.columns.length
3131
end
3232

3333
def test_columns_are_returned_in_the_order_they_were_declared
3434
column_names = Topic.columns.map { |column| column.name }
35-
assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id type), column_names
35+
assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id parent_title type), column_names
3636
end
3737

3838
def test_content_columns
3939
content_columns = Topic.content_columns
4040
content_column_names = content_columns.map {|column| column.name}
41-
assert_equal 8, content_columns.length
42-
assert_equal %w(title author_name author_email_address written_on bonus_time last_read content approved).sort, content_column_names.sort
41+
assert_equal 9, content_columns.length
42+
assert_equal %w(title author_name author_email_address written_on bonus_time last_read content approved parent_title).sort, content_column_names.sort
4343
end
4444

4545
def test_column_string_type_and_limit

activerecord/test/models/author.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ def testing_proxy_target
8787
has_many :tags, :through => :posts # through has_many :through
8888
has_many :post_categories, :through => :posts, :source => :categories
8989

90+
has_one :essay, :primary_key => :name, :as => :writer
91+
9092
belongs_to :author_address, :dependent => :destroy
9193
belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress"
9294

activerecord/test/models/company.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class Client < Company
8484
belongs_to :firm_with_select, :class_name => "Firm", :foreign_key => "firm_id", :select => "id"
8585
belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of"
8686
belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => ["1 = ?", 1]
87+
belongs_to :firm_with_primary_key, :class_name => "Firm", :primary_key => "name", :foreign_key => "firm_name"
8788
belongs_to :readonly_firm, :class_name => "Firm", :foreign_key => "firm_id", :readonly => true
8889

8990
# Record destruction so we can test whether firm.clients.clear has

0 commit comments

Comments
 (0)