Changeset 9067
- Timestamp:
- 03/21/08 18:09:03 (2 years ago)
- Files:
-
- trunk/activerecord/CHANGELOG (modified) (1 diff)
- trunk/activerecord/lib/active_record/association_preload.rb (modified) (3 diffs)
- trunk/activerecord/lib/active_record/associations.rb (modified) (5 diffs)
- trunk/activerecord/lib/active_record/associations/has_one_through_association.rb (added)
- trunk/activerecord/test/cases/associations_test.rb (modified) (3 diffs)
- trunk/activerecord/test/cases/associations/join_model_test.rb (modified) (1 diff)
- trunk/activerecord/test/fixtures/clubs.yml (added)
- trunk/activerecord/test/fixtures/members.yml (added)
- trunk/activerecord/test/fixtures/memberships.yml (added)
- trunk/activerecord/test/fixtures/sponsors.yml (added)
- trunk/activerecord/test/models/club.rb (added)
- trunk/activerecord/test/models/member.rb (added)
- trunk/activerecord/test/models/membership.rb (added)
- trunk/activerecord/test/models/sponsor.rb (added)
- trunk/activerecord/test/schema/schema.rb (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/activerecord/CHANGELOG
r9056 r9067 1 1 *SVN* 2 3 * Add has_one :through support. #4756 [thechrisoshow] 2 4 3 5 * Migrations: create_table supports primary_key_prefix_type. #10314 [student, thechrisoshow] trunk/activerecord/lib/active_record/association_preload.rb
r8942 r9067 49 49 end 50 50 end 51 52 def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record) 53 parent_records.each do |parent_record| 54 association_proxy = parent_record.send(reflection_name) 55 association_proxy.loaded 56 association_proxy.target = associated_record 57 end 58 end 51 59 52 60 def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key) … … 98 106 99 107 def preload_has_one_association(records, reflection, preload_options={}) 100 id_to_record_map, ids = construct_id_map(records) 101 records.each {|record| record.send("set_#{reflection.name}_target", nil)} 102 103 set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), 104 reflection.primary_key_name) 108 id_to_record_map, ids = construct_id_map(records) 109 options = reflection.options 110 if options[:through] 111 records.each {|record| record.send(reflection.name) && record.send(reflection.name).loaded} 112 through_records = preload_through_records(records, reflection, options[:through]) 113 through_reflection = reflections[options[:through]] 114 through_primary_key = through_reflection.primary_key_name 115 unless through_records.empty? 116 source = reflection.source_reflection.name 117 through_records.first.class.preload_associations(through_records, source) 118 through_records.compact.each do |through_record| 119 add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_i], 120 reflection.name, through_record.send(source)) 121 end 122 end 123 else 124 records.each {|record| record.send("set_#{reflection.name}_target", nil)} 125 126 127 set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name) 128 end 105 129 end 106 130 … … 127 151 end 128 152 end 129 153 130 154 def preload_through_records(records, reflection, through_association) 131 155 through_reflection = reflections[through_association] trunk/activerecord/lib/active_record/associations.rb
r9064 r9067 7 7 require 'active_record/associations/has_many_through_association' 8 8 require 'active_record/associations/has_and_belongs_to_many_association' 9 require 'active_record/associations/has_one_through_association' 9 10 10 11 module ActiveRecord … … 738 739 # * <tt>:include</tt> - specify second-order associations that should be eager loaded when this object is loaded. 739 740 # * <tt>:as</tt>: Specifies a polymorphic interface (See <tt>#belongs_to</tt>). 741 # * <tt>:through</tt>: Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt> 742 # are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a 743 # <tt>has_one</tt> or <tt>belongs_to</tt> association on the join model. 744 # * <tt>:source</tt>: Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be 745 # inferred from the association. <tt>has_one :favorite, :through => :favorites</tt> will look for a 746 # <tt>:favorite</tt> on +Favorite+, unless a <tt>:source</tt> is given. 740 747 # * <tt>:readonly</tt> - if set to +true+, the associated object is readonly through the association. 741 748 # … … 747 754 # has_one :attachment, :as => :attachable 748 755 # has_one :boss, :readonly => :true 756 # has_one :club, :through => :membership 757 # has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable 749 758 def has_one(association_id, options = {}) 750 reflection = create_has_one_reflection(association_id, options) 751 752 ivar = "@#{reflection.name}" 753 754 method_name = "has_one_after_save_for_#{reflection.name}".to_sym 755 define_method(method_name) do 756 association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}") 757 758 if !association.nil? && (new_record? || association.new_record? || association["#{reflection.primary_key_name}"] != id) 759 association["#{reflection.primary_key_name}"] = id 760 association.save(true) 761 end 762 end 763 after_save method_name 764 765 association_accessor_methods(reflection, HasOneAssociation) 766 association_constructor_method(:build, reflection, HasOneAssociation) 767 association_constructor_method(:create, reflection, HasOneAssociation) 768 769 configure_dependency_for_has_one(reflection) 759 if options[:through] 760 reflection = create_has_one_through_reflection(association_id, options) 761 association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation) 762 else 763 reflection = create_has_one_reflection(association_id, options) 764 765 ivar = "@#{reflection.name}" 766 767 method_name = "has_one_after_save_for_#{reflection.name}".to_sym 768 define_method(method_name) do 769 association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}") 770 771 if !association.nil? && (new_record? || association.new_record? || association["#{reflection.primary_key_name}"] != id) 772 association["#{reflection.primary_key_name}"] = id 773 association.save(true) 774 end 775 end 776 after_save method_name 777 778 association_accessor_methods(reflection, HasOneAssociation) 779 association_constructor_method(:build, reflection, HasOneAssociation) 780 association_constructor_method(:create, reflection, HasOneAssociation) 781 782 configure_dependency_for_has_one(reflection) 783 end 770 784 end 771 785 … … 1059 1073 end 1060 1074 1061 association.replace(new_value) 1075 if association_proxy_class == HasOneThroughAssociation 1076 association.create_through_record(new_value) 1077 self.send(reflection.name, new_value) 1078 else 1079 association.replace(new_value) 1080 end 1062 1081 1063 1082 instance_variable_set(ivar, new_value.nil? ? nil : association) … … 1299 1318 ) 1300 1319 1320 create_reflection(:has_one, association_id, options, self) 1321 end 1322 1323 def create_has_one_through_reflection(association_id, options) 1324 options.assert_valid_keys( 1325 :class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source 1326 ) 1301 1327 create_reflection(:has_one, association_id, options, self) 1302 1328 end trunk/activerecord/test/cases/associations_test.rb
r8989 r9067 21 21 require 'models/treasure' 22 22 require 'models/price_estimate' 23 require 'models/club' 24 require 'models/member' 25 require 'models/membership' 26 require 'models/sponsor' 23 27 24 28 class AssociationsTest < ActiveRecord::TestCase … … 187 191 assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit 188 192 end 189 193 190 194 def test_has_one_cache_nils 191 195 firm = companies(:another_firm) … … 477 481 end 478 482 483 class HasOneThroughAssociationsTest < ActiveRecord::TestCase 484 fixtures :members, :clubs, :memberships, :sponsors 485 486 def setup 487 @member = members(:groucho) 488 end 489 490 def test_has_one_through_with_has_one 491 assert_equal clubs(:boring_club), @member.club 492 end 493 494 def test_has_one_through_with_has_many 495 assert_equal clubs(:moustache_club), @member.favourite_club 496 end 497 498 def test_creating_association_creates_through_record 499 new_member = Member.create(:name => "Chris") 500 new_member.club = Club.create(:name => "LRUG") 501 assert_not_nil new_member.current_membership 502 assert_not_nil new_member.club 503 end 504 505 def test_replace_target_record 506 new_club = Club.create(:name => "Marx Bros") 507 @member.club = new_club 508 @member.reload 509 assert_equal new_club, @member.club 510 end 511 512 def test_replacing_target_record_deletes_old_association 513 assert_no_difference "Membership.count" do 514 new_club = Club.create(:name => "Bananarama") 515 @member.club = new_club 516 @member.reload 517 end 518 end 519 520 def test_has_one_through_polymorphic 521 assert_equal clubs(:moustache_club), @member.sponsor_club 522 end 523 524 def has_one_through_to_has_many 525 assert_equal 2, @member.fellow_members.size 526 end 527 528 def test_has_one_through_eager_loading 529 members = Member.find(:all, :include => :club) 530 assert_equal 2, members.size 531 assert_not_nil assert_no_queries {members[0].club} 532 end 533 534 def test_has_one_through_eager_loading_through_polymorphic 535 members = Member.find(:all, :include => :sponsor_club) 536 assert_equal 2, members.size 537 assert_not_nil assert_no_queries {members[0].sponsor_club} 538 end 539 end 479 540 480 541 class HasManyAssociationsTest < ActiveRecord::TestCase trunk/activerecord/test/cases/associations/join_model_test.rb
r9022 r9067 632 632 end 633 633 end 634 634 635 635 private 636 636 # create dynamic Post models to allow different dependency options trunk/activerecord/test/schema/schema.rb
r8969 r9067 50 50 t.integer :post_id, :null => false 51 51 end 52 53 create_table :clubs, :force => true do |t| 54 t.string :name 55 end 52 56 53 57 create_table :colnametests, :force => true do |t| … … 116 120 t.integer :tps_report_number 117 121 t.integer :version, :null => false, :default => 0 122 end 123 124 create_table :members, :force => true do |t| 125 t.string :name 126 end 127 128 create_table :memberships, :force => true do |t| 129 t.datetime :joined_on 130 t.integer :club_id, :member_id 131 t.boolean :favourite, :default => false 132 t.string :type 118 133 end 119 134 … … 177 192 t.integer :post_id, :null => false 178 193 t.integer :person_id, :null => false 194 end 195 196 create_table :sponsors, :force => true do |t| 197 t.integer :club_id 198 t.integer :sponsorable_id 199 t.integer :sponsorable_type 179 200 end 180 201