Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 4786

Show
Ignore:
Timestamp:
08/18/06 07:35:07 (2 years ago)
Author:
bitsweat
Message:

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.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/activerecord/CHANGELOG

    r4783 r4786  
    11*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 
    214 
    315* Cache nil results for :included has_one associations also.  #5787 [Michael Schoen] 
  • trunk/activerecord/lib/active_record/associations.rb

    r4783 r4786  
    3636      source_reflection  = reflection.source_reflection 
    3737      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.") 
    3844    end 
    3945  end 
  • trunk/activerecord/lib/active_record/associations/has_many_through_association.rb

    r4640 r4786  
    2323          options[:order] = @reflection.options[:order] 
    2424        end 
    25          
     25 
    2626        options[:select]  = construct_select(options[:select]) 
    2727        options[:from]  ||= construct_from 
    2828        options[:joins]   = construct_joins(options[:joins]) 
    2929        options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? 
    30          
     30 
    3131        merge_options_from_reflection!(options) 
    3232 
     
    4141      end 
    4242 
     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. 
    4347      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 
    5173      # Calculate sum using SQL, not Enumerable 
    5274      def sum(*args, &block) 
    5375        calculate(:sum, *args, &block) 
    5476      end 
    55        
     77 
    5678      protected 
    5779        def method_missing(method, *args, &block) 
     
    6486 
    6587        def find_target 
    66           records = @reflection.klass.find(:all,  
     88          records = @reflection.klass.find(:all, 
    6789            :select     => construct_select, 
    6890            :conditions => construct_conditions, 
    6991            :from       => construct_from, 
    7092            :joins      => construct_joins, 
    71             :order      => @reflection.options[:order],  
     93            :order      => @reflection.options[:order], 
    7294            :limit      => @reflection.options[:limit], 
    7395            :group      => @reflection.options[:group], 
     
    78100        end 
    79101 
     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 
    80122        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 (') + ")" 
    90129        end 
    91130 
     
    93132          @reflection.table_name 
    94133        end 
    95          
     134 
    96135        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) 
    101140          polymorphic_join = nil 
    102141          if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to 
     
    121160          ] 
    122161        end 
    123          
     162 
    124163        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 
    131171        def construct_sql 
    132172          case 
     
    148188          end 
    149189        end 
    150          
     190 
    151191        def conditions 
    152192          @conditions ||= [ 
     
    155195          ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions]) 
    156196        end 
    157          
     197 
    158198        alias_method :sql_conditions, :conditions 
    159199    end 
  • trunk/activerecord/test/associations_join_model_test.rb

    r4640 r4786  
    364364  end 
    365365 
    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 
    374384  def test_has_many_through_sum_uses_calculations 
    375385    assert_nothing_raised { authors(:david).comments.sum(:post_id) }