Created
April 27, 2012 16:45
-
-
Save TylerRick/2510696 to your computer and use it in GitHub Desktop.
composite_primary_keys: association.build for has_many should populate newly built child record with owner's PK values
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The FK attributes in the new record used to be set by set_belongs_to_association_for | |
(which the composite_primary_keys was overriding to work with CPK in its AR 3.0 series), | |
until this change in ActiveRecord: | |
commit e8ada11aac28f0850f0e485acacf34e7eb81aa19 | |
Author: Jon Leighton <[email protected]> | |
Date: Fri Dec 24 00:29:04 2010 +0000 | |
Associations: DRY up the code which is generating conditions, and make it all use arel rather than SQL strings | |
--- activerecord/lib/active_record/associations/association_collection.rb | |
+++ activerecord/lib/active_record/associations/association_collection.rb | |
@@ -111,7 +111,7 @@ module ActiveRecord | |
else | |
build_record(attributes) do |record| | |
block.call(record) if block_given? | |
- set_belongs_to_association_for(record) | |
+ set_owner_attributes(record) | |
end | |
You might think from looking at this diff that now they're set via set_owner_attributes now instead of set_belongs_to_association_for, but that appears to not be the case either. | |
Instead they're set directly within build_record, as shown in this debugger trace: | |
========================================= | |
With simple FK: | |
[230, 239] in /home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/associations/association.rb | |
230 @reflection.klass | |
231 end | |
232 | |
233 def build_record(attributes, options) | |
234 reflection.build_association(attributes, options) do |record| | |
=> 235 attributes = create_scope.except(*(record.changed - [reflection.foreign_key])) | |
236 record.assign_attributes(attributes, :without_protection => true) | |
237 end | |
238 end | |
239 end | |
/home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/associations/association.rb:235 | |
attributes = create_scope.except(*(record.changed - [reflection.foreign_key])) | |
(rdb:1) create_scope | |
{"user_id"=>1} | |
(rdb:1) reflection.foreign_key | |
"user_id" | |
(rdb:1) record | |
#<Reading id: nil, user_id: nil, article_id: nil, rating: nil> | |
(rdb:1) record.changed | |
[] | |
(rdb:1) create_scope.except(*(record.changed - [reflection.foreign_key])) | |
{"user_id"=>1} | |
(rdb:1) | |
[441, 450] in /home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/associations/collection_association.rb | |
441 def insert_record(record, validate = true, raise = false) | |
442 raise NotImplementedError | |
443 end | |
444 | |
445 def create_scope | |
=> 446 scoped.scope_for_create.stringify_keys | |
447 end | |
448 | |
449 def delete_or_destroy(records, method) | |
450 records = records.flatten | |
/home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/associations/collection_association.rb:446 | |
scoped.scope_for_create.stringify_keys | |
[464, 473] in /home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/relation.rb | |
464 | |
465 Hash[equalities.map { |where| [where.left.name, where.right] }] | |
466 end | |
467 | |
468 def scope_for_create | |
=> 469 @scope_for_create ||= where_values_hash.merge(create_with_value) | |
470 end | |
471 | |
472 def eager_loading? | |
473 @should_eager_load ||= | |
/home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/relation.rb:469 | |
@scope_for_create ||= where_values_hash.merge(create_with_value) | |
(rdb:1) @scope_for_create | |
nil | |
(rdb:1) where_values_hash | |
{"user_id"=>1} | |
(rdb:1) create_with_value | |
{} | |
[456, 465] in /home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/relation.rb | |
456 def to_sql | |
457 @to_sql ||= klass.connection.to_sql(arel, @bind_values.dup) | |
458 end | |
459 | |
460 def where_values_hash | |
=> 461 equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| | |
462 node.left.relation.name == table_name | |
463 } | |
464 | |
465 Hash[equalities.map { |where| [where.left.name, where.right] }] | |
/home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/relation.rb:461 | |
equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| | |
(rdb:1) with_default_scope | |
[#<Reading id: 1, user_id: 1, article_id: 1, rating: 4>, #<Reading id: 2, user_id: 1, article_id: 2, rating: 5>] | |
(rdb:1) table_name | |
"readings" | |
(rdb:1) with_default_scope.where_values.grep(Arel::Nodes::Equality) | |
[#<Arel::Nodes::Equality:0x00000001a08f38 @left=#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x00000001a04758 @name="readings", @engine=ActiveRecord::Base, @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>, name="user_id">, @right=1>] | |
(rdb:1) with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| node.left.relation.name == table_name } | |
[#<Arel::Nodes::Equality:0x00000001a08f38 @left=#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x00000001a04758 @name="readings", @engine=ActiveRecord::Base, @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>, name="user_id">, @right=1>] | |
(rdb:1) Hash[equalities.map { |where| [where.left.name, where.right] }] | |
{"user_id"=>1} | |
===================================== | |
With CPK: | |
[441, 450] in /home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/associations/collection_association.rb | |
441 def insert_record(record, validate = true, raise = false) | |
442 raise NotImplementedError | |
443 end | |
444 | |
445 def create_scope | |
=> 446 scoped.scope_for_create.stringify_keys | |
447 end | |
448 | |
449 def delete_or_destroy(records, method) | |
450 records = records.flatten | |
/home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/associations/collection_association.rb:446 | |
scoped.scope_for_create.stringify_keys | |
(rdb:1) scoped.scope_for_create | |
{} | |
(rdb:1) s | |
[82, 91] in /home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/associations/association.rb | |
82 @target = target | |
83 loaded! | |
84 end | |
85 | |
86 def scoped | |
=> 87 target_scope.merge(association_scope) | |
88 end | |
89 | |
90 # The scope for this association. | |
91 # | |
Potential problem (unless we override build_record in CPK): | |
[reflection.foreign_key] is an array of arrays instead of a flattened array! | |
so if record.changed included one of the fields in the composite FK (which I think it might if say :department_id was passed as arg to build), the create_scope will not override the passed-in values... | |
I think the create_scope *should* override the supplied args though, because it's a foreign key and shoudln't be messed with. | |
[230, 239] in /home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/associations/association.rb | |
230 @reflection.klass | |
231 end | |
232 | |
233 def build_record(attributes, options) | |
234 reflection.build_association(attributes, options) do |record| | |
=> 235 attributes = create_scope.except(*(record.changed - [reflection.foreign_key])) | |
236 record.assign_attributes(attributes, :without_protection => true) | |
237 end | |
238 end | |
239 end | |
/home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/associations/association.rb:235 | |
attributes = create_scope.except(*(record.changed - [reflection.foreign_key])) | |
(rdb:1) record.changed | |
[] | |
(rdb:1) record | |
#<Employee id: nil, department_id: nil, location_id: nil> | |
(rdb:1) reflection.foreign_key | |
[:department_id, :location_id] | |
(rdb:1) [reflection.foreign_key] | |
[[:department_id, :location_id]] | |
[464, 473] in /home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/relation.rb | |
464 | |
465 Hash[equalities.map { |where| [where.left.name, where.right] }] | |
466 end | |
467 | |
468 def scope_for_create | |
=> 469 @scope_for_create ||= where_values_hash.merge(create_with_value) | |
470 end | |
471 | |
472 def eager_loading? | |
473 @should_eager_load ||= | |
/home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/relation.rb:469 | |
@scope_for_create ||= where_values_hash.merge(create_with_value) | |
(rdb:1) create_with_value | |
{} | |
(rdb:1) where_values_hash | |
{} | |
Here's where the problem is: | |
[456, 465] in /home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/relation.rb | |
456 def to_sql | |
457 @to_sql ||= klass.connection.to_sql(arel, @bind_values.dup) | |
458 end | |
459 | |
460 def where_values_hash | |
=> 461 equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| | |
462 node.left.relation.name == table_name | |
463 } | |
464 | |
465 Hash[equalities.map { |where| [where.left.name, where.right] }] | |
/home/tyler/.rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.3/lib/active_record/relation.rb:461 | |
equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| | |
(rdb:1) with_default_scope | |
[#<Employee id: 3, department_id: 2, location_id: 1>, #<Employee id: 4, department_id: 2, location_id: 1>] | |
(rdb:1) with_default_scope.where_values.grep(Arel::Nodes::Equality) | |
[] | |
(rdb:1) with_default_scope.where_values.size | |
1 | |
(rdb:1) with_default_scope.where_values | |
[#<Arel::Nodes::And:0x00000002d6d8d0 @children=[#<Arel::Nodes::Equality:0x00000002d6d920 @left=#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x00000002d6dd58 @name="employees", @engine=ActiveRecord::Base, @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>, name=:department_id>, @right=2>, #<Arel::Nodes::Equality:0x00000002d6d8f8 @left=#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x00000002d6dd58 @name="employees", @engine=ActiveRecord::Base, @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>, name=:location_id>, @right=1>]>] | |
It's looking for elements in the where_values array that are of type Arel::Nodes::Equality, but in our case the only element in the array is of type Arel::Nodes::And -- which of course is what joins our two (or more) Arel::Nodes::Equality nodes together for our composite key, but the grep isn't smart enough to look at the *children* of the Arel::Nodes::And to find the Arel::Nodes::Equality nodes! | |
So we just need to change where_values_hash to look inside of the And node so that it will work with CPK! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See composite-primary-keys/composite_primary_keys#103