Changeset 4786
- Timestamp:
- 08/18/06 07:35:07 (2 years ago)
- Files:
-
- trunk/activerecord/CHANGELOG (modified) (1 diff)
- trunk/activerecord/lib/active_record/associations.rb (modified) (1 diff)
- trunk/activerecord/lib/active_record/associations/has_many_through_association.rb (modified) (8 diffs)
- trunk/activerecord/test/associations_join_model_test.rb (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/activerecord/CHANGELOG
r4783 r4786 1 1 *SVN* 2 3 * Add records to has_many :through using <<, push, and concat by creating the association record. Raise if base or associate are new records since both ids are required to create the association. #build raises since you can't associate an unsaved record. #create! takes an attributes hash and creates the associated record and its association in a transaction. [Jeremy Kemper] 4 5 # Create a tagging to associate the post and tag. 6 post.tags << Tag.find_by_name('old') 7 post.tags.create! :name => 'general' 8 9 # Would have been: 10 post.taggings.create!(:tag => Tag.find_by_name('finally') 11 transaction do 12 post.taggings.create!(:tag => Tag.create!(:name => 'general')) 13 end 2 14 3 15 * Cache nil results for :included has_one associations also. #5787 [Michael Schoen] trunk/activerecord/lib/active_record/associations.rb
r4783 r4786 36 36 source_reflection = reflection.source_reflection 37 37 super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}. Use :source to specify the source reflection.") 38 end 39 end 40 41 class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc: 42 def initialize(owner, reflection) 43 super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.") 38 44 end 39 45 end trunk/activerecord/lib/active_record/associations/has_many_through_association.rb
r4640 r4786 23 23 options[:order] = @reflection.options[:order] 24 24 end 25 25 26 26 options[:select] = construct_select(options[:select]) 27 27 options[:from] ||= construct_from 28 28 options[:joins] = construct_joins(options[:joins]) 29 29 options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? 30 30 31 31 merge_options_from_reflection!(options) 32 32 … … 41 41 end 42 42 43 # Adds records to the association. The source record and its associates 44 # must have ids in order to create records associating them, so this 45 # will raise ActiveRecord::HasManyThroughCantAssociateNewRecords if 46 # either is a new record. Calls create! so you can rescue errors. 43 47 def <<(*args) 44 raise ActiveRecord::ReadOnlyAssociation.new(@reflection) 45 end 46 47 [:push, :concat, :create, :build].each do |method| 48 alias_method method, :<< 49 end 50 48 return if args.empty? 49 through = @reflection.through_reflection 50 raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) if @owner.new_record? 51 52 klass = through.klass 53 klass.transaction do 54 args.each do |associate| 55 raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record? 56 klass.with_scope(:create => construct_association_attributes(through)) { klass.create! } 57 end 58 end 59 end 60 61 [:push, :concat].each { |method| alias_method method, :<< } 62 63 def build(attrs = nil) 64 raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, @reflection.through_reflection) 65 end 66 67 def create!(attrs = nil) 68 @reflection.klass.transaction do 69 self << @reflection.klass.with_scope(:create => attrs) { @reflection.klass.create! } 70 end 71 end 72 51 73 # Calculate sum using SQL, not Enumerable 52 74 def sum(*args, &block) 53 75 calculate(:sum, *args, &block) 54 76 end 55 77 56 78 protected 57 79 def method_missing(method, *args, &block) … … 64 86 65 87 def find_target 66 records = @reflection.klass.find(:all, 88 records = @reflection.klass.find(:all, 67 89 :select => construct_select, 68 90 :conditions => construct_conditions, 69 91 :from => construct_from, 70 92 :joins => construct_joins, 71 :order => @reflection.options[:order], 93 :order => @reflection.options[:order], 72 94 :limit => @reflection.options[:limit], 73 95 :group => @reflection.options[:group], … … 78 100 end 79 101 102 def construct_association_attributes(reflection) 103 if as = reflection.options[:as] 104 { "#{as}_id" => @owner.id, 105 "#{as}_type" => @owner.class.base_class.name.to_s } 106 else 107 { reflection.primary_key_name => @owner.id } 108 end 109 end 110 111 def construct_quoted_association_attributes(reflection) 112 if as = reflection.options[:as] 113 { "#{as}_id" => @owner.quoted_id, 114 "#{as}_type" => reflection.klass.quote( 115 @owner.class.base_class.name.to_s, 116 reflection.klass.columns_hash["#{as}_type"]) } 117 else 118 { reflection.primary_key_name => @owner.quoted_id } 119 end 120 end 121 80 122 def construct_conditions 81 conditions = if @reflection.through_reflection.options[:as] 82 "#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.options[:as]}_id = #{@owner.quoted_id} " + 83 "AND #{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.options[:as]}_type = #{@owner.class.quote @owner.class.base_class.name.to_s}" 84 else 85 "#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.primary_key_name} = #{@owner.quoted_id}" 86 end 87 conditions << " AND (#{sql_conditions})" if sql_conditions 88 89 return conditions 123 table_name = @reflection.through_reflection.table_name 124 conditions = construct_quoted_association_attributes(@reflection.through_reflection).map do |attr, value| 125 "#{table_name}.#{attr} = #{value}" 126 end 127 conditions << sql_conditions if sql_conditions 128 "(" + conditions.join(') AND (') + ")" 90 129 end 91 130 … … 93 132 @reflection.table_name 94 133 end 95 134 96 135 def construct_select(custom_select = nil) 97 selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*" 98 end 99 100 def construct_joins(custom_joins = nil) 136 selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*" 137 end 138 139 def construct_joins(custom_joins = nil) 101 140 polymorphic_join = nil 102 141 if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to … … 121 160 ] 122 161 end 123 162 124 163 def construct_scope 125 { 126 :find => { :from => construct_from, :conditions => construct_conditions, :joins => construct_joins, :select => construct_select }, 127 :create => { @reflection.primary_key_name => @owner.id } 128 } 129 end 130 164 { :create => construct_association_attributes(@reflection), 165 :find => { :from => construct_from, 166 :conditions => construct_conditions, 167 :joins => construct_joins, 168 :select => construct_select } } 169 end 170 131 171 def construct_sql 132 172 case … … 148 188 end 149 189 end 150 190 151 191 def conditions 152 192 @conditions ||= [ … … 155 195 ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions]) 156 196 end 157 197 158 198 alias_method :sql_conditions, :conditions 159 199 end trunk/activerecord/test/associations_join_model_test.rb
r4640 r4786 364 364 end 365 365 366 def test_raise_error_when_adding_to_has_many_through 367 assert_raise(ActiveRecord::ReadOnlyAssociation) { posts(:thinking).tags << tags(:general) } 368 assert_raise(ActiveRecord::ReadOnlyAssociation) { posts(:thinking).tags.push tags(:general) } 369 assert_raise(ActiveRecord::ReadOnlyAssociation) { posts(:thinking).tags.concat tags(:general) } 370 assert_raise(ActiveRecord::ReadOnlyAssociation) { posts(:thinking).tags.build(:name => 'foo') } 371 assert_raise(ActiveRecord::ReadOnlyAssociation) { posts(:thinking).tags.create(:name => 'foo') } 372 end 373 366 def test_raise_error_when_adding_new_record_to_has_many_through 367 assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).tags << tags(:general).clone } 368 assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).clone.tags << tags(:general) } 369 assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).tags.build } 370 end 371 372 def test_create_associate_when_adding_to_has_many_through 373 count = Tagging.count 374 assert_nothing_raised { posts(:thinking).tags << tags(:general) } 375 assert_equal(count + 1, Tagging.count) 376 377 assert_nothing_raised { posts(:thinking).tags.create!(:name => 'foo') } 378 assert_equal(count + 2, Tagging.count) 379 380 assert_nothing_raised { posts(:thinking).tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) } 381 assert_equal(count + 4, Tagging.count) 382 end 383 374 384 def test_has_many_through_sum_uses_calculations 375 385 assert_nothing_raised { authors(:david).comments.sum(:post_id) }