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

Changeset 9067

Show
Ignore:
Timestamp:
03/21/08 18:09:03 (2 years ago)
Author:
rick
Message:

Add has_one :through support, finally. Closes #4756 [thechrisoshow]

Files:

Legend:

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

    r9056 r9067  
    11*SVN* 
     2 
     3* Add has_one :through support.  #4756 [thechrisoshow] 
    24 
    35* Migrations: create_table supports primary_key_prefix_type.  #10314 [student, thechrisoshow] 
  • trunk/activerecord/lib/active_record/association_preload.rb

    r8942 r9067  
    4949        end 
    5050      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 
    5159 
    5260      def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key) 
     
    98106 
    99107      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                                        
    105129      end 
    106130 
     
    127151        end 
    128152      end 
    129  
     153       
    130154      def preload_through_records(records, reflection, through_association) 
    131155        through_reflection = reflections[through_association] 
  • trunk/activerecord/lib/active_record/associations.rb

    r9064 r9067  
    77require 'active_record/associations/has_many_through_association' 
    88require 'active_record/associations/has_and_belongs_to_many_association' 
     9require 'active_record/associations/has_one_through_association' 
    910 
    1011module ActiveRecord 
     
    738739      # * <tt>:include</tt>  - specify second-order associations that should be eager loaded when this object is loaded. 
    739740      # * <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.       
    740747      # * <tt>:readonly</tt> - if set to +true+, the associated object is readonly through the association. 
    741748      # 
     
    747754      #   has_one :attachment, :as => :attachable 
    748755      #   has_one :boss, :readonly => :true 
     756      #   has_one :club, :through => :membership 
     757      #   has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable 
    749758      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 
    770784      end 
    771785 
     
    10591073            end 
    10601074 
    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 
    10621081 
    10631082            instance_variable_set(ivar, new_value.nil? ? nil : association) 
     
    12991318          ) 
    13001319 
     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          ) 
    13011327          create_reflection(:has_one, association_id, options, self) 
    13021328        end 
  • trunk/activerecord/test/cases/associations_test.rb

    r8989 r9067  
    2121require 'models/treasure' 
    2222require 'models/price_estimate' 
     23require 'models/club' 
     24require 'models/member' 
     25require 'models/membership' 
     26require 'models/sponsor' 
    2327 
    2428class AssociationsTest < ActiveRecord::TestCase 
     
    187191    assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit 
    188192  end 
    189  
     193   
    190194  def test_has_one_cache_nils 
    191195    firm = companies(:another_firm) 
     
    477481end 
    478482 
     483class 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 
     539end 
    479540 
    480541class HasManyAssociationsTest < ActiveRecord::TestCase 
  • trunk/activerecord/test/cases/associations/join_model_test.rb

    r9022 r9067  
    632632    end 
    633633  end 
    634  
     634   
    635635  private 
    636636    # create dynamic Post models to allow different dependency options 
  • trunk/activerecord/test/schema/schema.rb

    r8969 r9067  
    5050      t.integer :post_id, :null => false 
    5151    end 
     52     
     53    create_table :clubs, :force => true do |t| 
     54      t.string :name 
     55    end 
    5256 
    5357    create_table :colnametests, :force => true do |t| 
     
    116120      t.integer :tps_report_number 
    117121      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 
    118133    end 
    119134 
     
    177192      t.integer :post_id, :null => false 
    178193      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 
    179200    end 
    180201