Skip to content

Instantly share code, notes, and snippets.

@TylerRick
Created April 27, 2012 16:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TylerRick/2510696 to your computer and use it in GitHub Desktop.
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
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 <j@jonathanleighton.com>
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!
@TylerRick
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment