Changeset 9230
- Timestamp:
- 04/06/08 00:27:12 (8 months ago)
- Files:
-
- trunk/activerecord/CHANGELOG (modified) (1 diff)
- trunk/activerecord/lib/active_record/associations.rb (modified) (6 diffs)
- trunk/activerecord/lib/active_record/associations/association_collection.rb (modified) (2 diffs)
- trunk/activerecord/lib/active_record/associations/association_proxy.rb (modified) (1 diff)
- trunk/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb (modified) (1 diff)
- trunk/activerecord/lib/active_record/associations/has_many_association.rb (modified) (1 diff)
- trunk/activerecord/lib/active_record/associations/has_many_through_association.rb (modified) (5 diffs)
- trunk/activerecord/test/cases/associations_test.rb (modified) (2 diffs)
- trunk/activerecord/test/cases/associations/join_model_test.rb (modified) (1 diff)
- trunk/activerecord/test/models/post.rb (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/activerecord/CHANGELOG
r9229 r9230 1 1 *SVN* 2 3 * Refactor HasManyThroughAssociation to inherit from HasManyAssociation. Association callbacks and <association>_ids= now work with hm:t. #11516 [rubyruy] 2 4 3 5 * Ensure HABTM#create and HABTM#build do not load entire association. [Pratik] trunk/activerecord/lib/active_record/associations.rb
r9226 r9230 45 45 end 46 46 47 class HasManyThroughCantAssociateThroughHasManyReflection < ActiveRecordError #:nodoc: 48 def initialize(owner, reflection) 49 super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") 50 end 51 end 47 52 class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc: 48 53 def initialize(owner, reflection) … … 126 131 # ----------------------------------+-------+----------+---------- 127 132 # #others | X | X | X 128 # #others=(other,other,...) | X | X | 133 # #others=(other,other,...) | X | X | X 129 134 # #other_ids | X | X | X 130 # #other_ids=(id,id,...) | X | X | 135 # #other_ids=(id,id,...) | X | X | X 131 136 # #others<< | X | X | X 132 137 # #others.push | X | X | X 133 138 # #others.concat | X | X | X 134 # #others.build(attributes={}) | X | X | 135 # #others.create(attributes={}) | X | X | 139 # #others.build(attributes={}) | X | X | X 140 # #others.create(attributes={}) | X | X | X 136 141 # #others.create!(attributes={}) | X | X | X 137 142 # #others.size | X | X | X … … 140 145 # #others.sum(args*,&block) | X | X | X 141 146 # #others.empty? | X | X | X 142 # #others.clear | X | X | 147 # #others.clear | X | X | X 143 148 # #others.delete(other,other,...) | X | X | X 144 149 # #others.delete_all | X | X | … … 146 151 # #others.find(*args) | X | X | X 147 152 # #others.find_first | X | | 148 # #others.uniq | X | X | 153 # #others.uniq | X | X | X 149 154 # #others.reset | X | X | X 150 155 # … … 651 656 # alongside this object by calling their destroy method. If set to <tt>:delete_all</tt> all associated 652 657 # objects are deleted *without* calling their destroy method. If set to <tt>:nullify</tt> all associated 653 # objects' foreign keys are set to +NULL+ *without* calling their save callbacks. 658 # objects' foreign keys are set to +NULL+ *without* calling their save callbacks. *Warning:* This option is ignored when also using 659 # the <tt>through</tt> option. 654 660 # * <tt>:finder_sql</tt> - specify a complete SQL statement to fetch the association. This is a good way to go for complex 655 661 # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added. … … 694 700 configure_dependency_for_has_many(reflection) 695 701 702 add_multiple_associated_save_callbacks(reflection.name) 703 add_association_callbacks(reflection.name, reflection.options) 704 696 705 if options[:through] 697 collection_accessor_methods(reflection, HasManyThroughAssociation , false)706 collection_accessor_methods(reflection, HasManyThroughAssociation) 698 707 else 699 add_multiple_associated_save_callbacks(reflection.name)700 add_association_callbacks(reflection.name, reflection.options)701 708 collection_accessor_methods(reflection, HasManyAssociation) 702 709 end trunk/activerecord/lib/active_record/associations/association_collection.rb
r9229 r9230 14 14 end 15 15 16 def build(attributes = {}) 17 if attributes.is_a?(Array) 18 attributes.collect { |attr| build(attr) } 19 else 20 build_record(attributes) { |record| set_belongs_to_association_for(record) } 21 end 22 end 23 16 24 # Add +records+ to this association. Returns +self+ so method calls may be chained. 17 25 # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. … … 56 64 records = flatten_deeper(records) 57 65 records.each { |record| raise_on_type_mismatch(record) } 58 records.reject! { |record| @target.delete(record) if record.new_record? } 66 records.reject! do |record| 67 if record.new_record? 68 callback(:before_remove, record) 69 @target.delete(record) 70 callback(:after_remove, record) 71 end 72 end 59 73 return if records.empty? 60 74 trunk/activerecord/lib/active_record/associations/association_proxy.rb
r9226 r9230 8 8 # BelongsToPolymorphicAssociation 9 9 # AssociationCollection 10 # HasAndBelongsToManyAssociation 10 11 # HasManyAssociation 11 # HasAndBelongsToManyAssociation 12 # HasManyThroughAssociation 13 # HasOneThroughAssociation 12 # HasManyThroughAssociation 13 # HasOneThroughAssociation 14 14 # 15 15 # Association proxies in Active Record are middlemen between the object that trunk/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
r9229 r9230 5 5 super 6 6 construct_sql 7 end8 9 def build(attributes = {})10 build_record(attributes)11 7 end 12 8 trunk/activerecord/lib/active_record/associations/has_many_association.rb
r9229 r9230 5 5 super 6 6 construct_sql 7 end8 9 def build(attributes = {})10 if attributes.is_a?(Array)11 attributes.collect { |attr| build(attr) }12 else13 build_record(attributes) { |record| set_belongs_to_association_for(record) }14 end15 7 end 16 8 trunk/activerecord/lib/active_record/associations/has_many_through_association.rb
r9200 r9230 1 1 module ActiveRecord 2 2 module Associations 3 class HasManyThroughAssociation < AssociationCollection #:nodoc:3 class HasManyThroughAssociation < HasManyAssociation #:nodoc: 4 4 def initialize(owner, reflection) 5 5 super 6 6 reflection.check_validity! 7 7 @finder_sql = construct_conditions 8 construct_sql9 end 8 end 9 10 10 11 11 def find(*args) … … 36 36 end 37 37 38 def reset39 @target = []40 @loaded = false41 end42 43 # Adds records to the association. The source record and its associates44 # must have ids in order to create records associating them, so this45 # will raise ActiveRecord::HasManyThroughCantAssociateNewRecords if46 # either is a new record. Calls create! so you can rescue errors.47 #48 # The :before_add and :after_add callbacks are not yet supported.49 def <<(*records)50 return if records.empty?51 through = @reflection.through_reflection52 raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) if @owner.new_record?53 54 klass = through.klass55 klass.transaction do56 flatten_deeper(records).each do |associate|57 raise_on_type_mismatch(associate)58 raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record?59 60 @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(associate)) { klass.create! }61 @target << associate if loaded?62 end63 end64 65 self66 end67 68 [:push, :concat].each { |method| alias_method method, :<< }69 70 # Removes +records+ from this association. Does not destroy +records+.71 def delete(*records)72 records = flatten_deeper(records)73 records.each { |associate| raise_on_type_mismatch(associate) }74 75 through = @reflection.through_reflection76 raise ActiveRecord::HasManyThroughCantDissociateNewRecords.new(@owner, through) if @owner.new_record?77 78 load_target79 80 klass = through.klass81 klass.transaction do82 flatten_deeper(records).each do |associate|83 raise_on_type_mismatch(associate)84 raise ActiveRecord::HasManyThroughCantDissociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record?85 86 klass.delete_all(construct_join_attributes(associate))87 @target.delete(associate)88 end89 end90 91 self92 end93 94 def build(attrs = nil)95 raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, @reflection.through_reflection)96 end97 38 alias_method :new, :build 98 39 … … 100 41 @reflection.klass.transaction do 101 42 self << (object = @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create! }) 43 object 44 end 45 end 46 47 def create(attrs = nil) 48 @reflection.klass.transaction do 49 self << (object = @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create }) 102 50 object 103 51 end … … 132 80 end 133 81 82 134 83 protected 84 def insert_record(record, force=true) 85 if record.new_record? 86 if force 87 record.save! 88 else 89 return false unless record.save 90 end 91 end 92 klass = @reflection.through_reflection.klass 93 @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { klass.create! } 94 end 95 96 # TODO - add dependent option support 97 def delete_records(records) 98 klass = @reflection.through_reflection.klass 99 records.each do |associate| 100 klass.delete_all(construct_join_attributes(associate)) 101 end 102 end 103 135 104 def method_missing(method, *args) 136 105 if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) … … 179 148 # Construct attributes for :through pointing to owner and associate. 180 149 def construct_join_attributes(associate) 150 # TODO: revist this to allow it for deletion, supposing dependent option is supported 151 raise ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection.new(@owner, @reflection) if @reflection.source_reflection.macro == :has_many 181 152 join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id) 182 153 if @reflection.options[:source_type] trunk/activerecord/test/cases/associations_test.rb
r9229 r9230 551 551 end 552 552 553 class HasManyThroughAssociationsTest < ActiveRecord::TestCase 554 fixtures :posts, :readers, :people 555 556 def test_associate_existing 557 assert_queries(2) { posts(:thinking);people(:david) } 558 559 assert_queries(1) do 560 posts(:thinking).people << people(:david) 561 end 562 563 assert_queries(1) do 564 assert posts(:thinking).people.include?(people(:david)) 565 end 566 567 assert posts(:thinking).reload.people(true).include?(people(:david)) 568 end 569 570 def test_associating_new 571 assert_queries(1) { posts(:thinking) } 572 new_person = nil # so block binding catches it 573 574 assert_queries(0) do 575 new_person = Person.new 576 end 577 578 # Associating new records always saves them 579 # Thus, 1 query for the new person record, 1 query for the new join table record 580 assert_queries(2) do 581 posts(:thinking).people << new_person 582 end 583 584 assert_queries(1) do 585 assert posts(:thinking).people.include?(new_person) 586 end 587 588 assert posts(:thinking).reload.people(true).include?(new_person) 589 end 590 591 def test_associate_new_by_building 592 assert_queries(1) { posts(:thinking) } 593 594 assert_queries(0) do 595 posts(:thinking).people.build(:first_name=>"Bob") 596 posts(:thinking).people.new(:first_name=>"Ted") 597 end 598 599 # Should only need to load the association once 600 assert_queries(1) do 601 assert posts(:thinking).people.collect(&:first_name).include?("Bob") 602 assert posts(:thinking).people.collect(&:first_name).include?("Ted") 603 end 604 605 # 2 queries for each new record (1 to save the record itself, 1 for the join model) 606 # * 2 new records = 4 607 # + 1 query to save the actual post = 5 608 assert_queries(5) do 609 posts(:thinking).save 610 end 611 612 assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Bob") 613 assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Ted") 614 end 615 616 def test_delete_association 617 assert_queries(2){posts(:welcome);people(:michael); } 618 619 assert_queries(1) do 620 posts(:welcome).people.delete(people(:michael)) 621 end 622 623 assert_queries(1) do 624 assert posts(:welcome).people.empty? 625 end 626 627 assert posts(:welcome).reload.people(true).empty? 628 end 629 630 def test_replace_association 631 assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)} 632 633 # 1 query to delete the existing reader (michael) 634 # 1 query to associate the new reader (david) 635 assert_queries(2) do 636 posts(:welcome).people = [people(:david)] 637 end 638 639 assert_queries(0){ 640 assert posts(:welcome).people.include?(people(:david)) 641 assert !posts(:welcome).people.include?(people(:michael)) 642 } 643 644 assert posts(:welcome).reload.people(true).include?(people(:david)) 645 assert !posts(:welcome).reload.people(true).include?(people(:michael)) 646 end 647 648 def test_associate_with_create 649 assert_queries(1) { posts(:thinking) } 650 651 # 1 query for the new record, 1 for the join table record 652 # No need to update the actual collection yet! 653 assert_queries(2) do 654 posts(:thinking).people.create(:first_name=>"Jeb") 655 end 656 657 # *Now* we actually need the collection so it's loaded 658 assert_queries(1) do 659 assert posts(:thinking).people.collect(&:first_name).include?("Jeb") 660 end 661 662 assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Jeb") 663 end 664 665 def test_clear_associations 666 assert_queries(2) { posts(:welcome);posts(:welcome).people(true) } 667 668 assert_queries(1) do 669 posts(:welcome).people.clear 670 end 671 672 assert_queries(0) do 673 assert posts(:welcome).people.empty? 674 end 675 676 assert posts(:welcome).reload.people(true).empty? 677 end 678 679 def test_association_callback_ordering 680 Post.reset_log 681 log = Post.log 682 post = posts(:thinking) 683 684 post.people_with_callbacks << people(:michael) 685 assert_equal [ 686 [:added, :before, "Michael"], 687 [:added, :after, "Michael"] 688 ], log.last(2) 689 690 post.people_with_callbacks.push(people(:david), Person.create!(:first_name => "Bob"), Person.new(:first_name => "Lary")) 691 assert_equal [ 692 [:added, :before, "David"], 693 [:added, :after, "David"], 694 [:added, :before, "Bob"], 695 [:added, :after, "Bob"], 696 [:added, :before, "Lary"], 697 [:added, :after, "Lary"] 698 ],log.last(6) 699 700 post.people_with_callbacks.build(:first_name => "Ted") 701 assert_equal [ 702 [:added, :before, "Ted"], 703 [:added, :after, "Ted"] 704 ], log.last(2) 705 706 post.people_with_callbacks.create(:first_name => "Sam") 707 assert_equal [ 708 [:added, :before, "Sam"], 709 [:added, :after, "Sam"] 710 ], log.last(2) 711 712 post.people_with_callbacks = [people(:michael),people(:david), Person.new(:first_name => "Julian"), Person.create!(:first_name => "Roger")] 713 assert_equal (%w(Ted Bob Sam Lary) * 2).sort, log[-12..-5].collect(&:last).sort 714 assert_equal [ 715 [:added, :before, "Julian"], 716 [:added, :after, "Julian"], 717 [:added, :before, "Roger"], 718 [:added, :after, "Roger"] 719 ], log.last(4) 720 721 post.people_with_callbacks.clear 722 assert_equal (%w(Michael David Julian Roger) * 2).sort, log.last(8).collect(&:last).sort 723 end 724 end 725 553 726 class HasManyAssociationsTest < ActiveRecord::TestCase 554 fixtures :accounts, :c ompanies, :developers, :projects,727 fixtures :accounts, :categories, :companies, :developers, :projects, 555 728 :developers_projects, :topics, :authors, :comments, :author_addresses, 556 729 :people, :posts … … 1292 1465 end 1293 1466 1294 def test_assign_ids_for_through 1295 assert_raise(NoMethodError) { authors(:mary).comment_ids = [123] } 1467 def test_modifying_a_through_a_has_many_should_raise 1468 [ 1469 lambda { authors(:mary).comment_ids = [comments(:greetings).id, comments(:more_greetings).id] }, 1470 lambda { authors(:mary).comments = [comments(:greetings), comments(:more_greetings)] }, 1471 lambda { authors(:mary).comments << Comment.create!(:body => "Yay", :post_id => 424242) }, 1472 lambda { authors(:mary).comments.delete(authors(:mary).comments.first) }, 1473 ].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection, &block) } 1474 end 1475 1476 1477 def test_assign_ids_for_through_a_belongs_to 1478 post = Post.new(:title => "Assigning IDs works!", :body => "You heared it here first, folks!") 1479 post.person_ids = [people(:david).id, people(:michael).id] 1480 post.save 1481 post.reload 1482 assert_equal 2, post.people.length 1483 assert post.people.include?(people(:david)) 1296 1484 end 1297 1485 trunk/activerecord/test/cases/associations/join_model_test.rb
r9200 r9230 444 444 end 445 445 446 def test_raise_error_when_adding_new_record_to_has_many_through 447 assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).tags << tags(:general).clone } 448 assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).clone.tags << tags(:general) } 449 assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).tags.build } 450 assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).tags.new } 446 def test_associating_unsaved_records_with_has_many_through 447 saved_post = posts(:thinking) 448 new_tag = Tag.new(:name => "new") 449 450 saved_post.tags << new_tag 451 assert !new_tag.new_record? #consistent with habtm! 452 assert !saved_post.new_record? 453 assert saved_post.tags.include?(new_tag) 454 455 assert !new_tag.new_record? 456 assert saved_post.reload.tags(true).include?(new_tag) 457 458 459 new_post = Post.new(:title => "Association replacmenet works!", :body => "You best believe it.") 460 saved_tag = tags(:general) 461 462 new_post.tags << saved_tag 463 assert new_post.new_record? 464 assert !saved_tag.new_record? 465 assert new_post.tags.include?(saved_tag) 466 467 new_post.save! 468 assert !new_post.new_record? 469 assert new_post.reload.tags(true).include?(saved_tag) 470 471 assert posts(:thinking).tags.build.new_record? 472 assert posts(:thinking).tags.new.new_record? 451 473 end 452 474 trunk/activerecord/test/models/post.rb
r9084 r9230 47 47 has_many :readers 48 48 has_many :people, :through => :readers 49 has_many :people_with_callbacks, :source=>:person, :through => :readers, 50 :before_add => lambda {|owner, reader| log(:added, :before, reader.first_name) }, 51 :after_add => lambda {|owner, reader| log(:added, :after, reader.first_name) }, 52 :before_remove => lambda {|owner, reader| log(:removed, :before, reader.first_name) }, 53 :after_remove => lambda {|owner, reader| log(:removed, :after, reader.first_name) } 54 55 def self.reset_log 56 @log = [] 57 end 58 59 def self.log(message=nil, side=nil, new_record=nil) 60 return @log if message.nil? 61 @log << [message, side, new_record] 62 end 49 63 50 64 def self.what_are_you