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

Changeset 3769

Show
Ignore:
Timestamp:
03/04/06 23:33:10 (3 years ago)
Author:
david
Message:

Added cascading eager loading that allows for queries like Author.find(:all, :include=> { :posts=> :comments }), which will fetch all authors, their posts, and the comments belonging to those posts in a single query (using LEFT OUTER JOIN) #3913 [anna@wota.jp]

Files:

Legend:

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

    r3762 r3769  
    11*SVN* 
    22 
     3* Added cascading eager loading that allows for queries like Author.find(:all, :include=> { :posts=> :comments }), which will fetch all authors, their posts, and the comments belonging to those posts in a single query (using LEFT OUTER JOIN) #3913 [anna@wota.jp]. Examples: 
     4 
     5    # cascaded in two levels 
     6    >> Author.find(:all, :include=>{:posts=>:comments}) 
     7    => authors 
     8         +- posts 
     9              +- comments 
     10     
     11    # cascaded in two levels and normal association 
     12    >> Author.find(:all, :include=>[{:posts=>:comments}, :categorizations]) 
     13    => authors 
     14         +- posts 
     15              +- comments 
     16         +- categorizations 
     17     
     18    # cascaded in two levels with two has_many associations 
     19    >> Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}) 
     20    => authors 
     21         +- posts 
     22              +- comments 
     23              +- categorizations 
     24     
     25    # cascaded in three levels 
     26    >> Company.find(:all, :include=>{:groups=>{:members=>{:favorites}}}) 
     27    => companies 
     28         +- groups 
     29              +- members 
     30                   +- favorites 
     31     
    332* Make counter cache work when replacing an association #3245 [eugenol@gmail.com] 
    433 
  • trunk/activerecord/lib/active_record/associations.rb

    r3756 r3769  
    782782         
    783783        def count_with_associations(options = {}) 
    784           reflections = reflect_on_included_associations(options[:include]) 
    785           return count_by_sql(construct_counter_sql_with_included_associations(options, reflections)) 
     784          join_dependency = JoinDependency.new(self, options[:include]) 
     785          return count_by_sql(construct_counter_sql_with_included_associations(options, join_dependency)) 
    786786        end 
    787787 
    788788        def find_with_associations(options = {}) 
    789           reflections  = reflect_on_included_associations(options[:include]) 
    790  
    791           guard_against_missing_reflections(reflections, options) 
    792            
    793           schema_abbreviations = generate_schema_abbreviations(reflections) 
    794           primary_key_table    = generate_primary_key_table(reflections, schema_abbreviations) 
    795  
    796           rows                      = select_all_rows(options, schema_abbreviations, reflections) 
    797           records, records_in_order = { }, [] 
    798           primary_key               = primary_key_table[table_name] 
    799            
    800           for row in rows 
    801             id = row[primary_key] 
    802             records_in_order << (records[id] = instantiate(extract_record(schema_abbreviations, table_name, row))) unless records[id] 
    803             record = records[id] 
    804  
    805             reflections.each do |reflection| 
    806               case reflection.macro 
    807                 when :has_many, :has_and_belongs_to_many 
    808                   collection = record.send(reflection.name) 
    809                   collection.loaded 
    810  
    811                   next unless row[primary_key_table[reflection.table_name]] 
    812  
    813                   association = reflection.klass.send(:instantiate, extract_record(schema_abbreviations, reflection.table_name, row))                   
    814                   collection.target.push(association) unless collection.target.include?(association) 
    815                 when :has_one, :belongs_to 
    816                   next unless row[primary_key_table[reflection.table_name]] 
    817  
    818                   record.send( 
    819                     "set_#{reflection.name}_target",  
    820                     reflection.klass.send(:instantiate, extract_record(schema_abbreviations, reflection.table_name, row)) 
    821                   ) 
    822               end 
    823             end 
    824           end 
    825            
    826           return records_in_order 
    827         end 
    828  
     789          join_dependency = JoinDependency.new(self, options[:include]) 
     790          rows = select_all_rows(options, join_dependency) 
     791          return join_dependency.instantiate(rows) 
     792            end 
    829793 
    830794        def configure_dependency_for_has_many(reflection) 
     
    940904        end 
    941905 
    942         def guard_against_missing_reflections(reflections, options) 
    943           reflections.each do |r|  
    944             raise( 
    945               ConfigurationError,  
    946               "Association was not found; perhaps you misspelled it?  " + 
    947               "You specified :include => :#{[options[:include]].flatten.join(', :')}" 
    948             ) if r.nil?  
    949           end 
    950         end 
    951          
    952906        def guard_against_unlimitable_reflections(reflections, options) 
    953907          if (options[:offset] || options[:limit]) && !using_limitable_reflections?(reflections) 
     
    959913        end 
    960914 
    961         def generate_schema_abbreviations(reflections) 
    962           schema = [ [ table_name, column_names ] ] 
    963           schema += reflections.collect { |r| [ r.table_name, r.klass.column_names ] } 
    964  
    965           schema_abbreviations = {} 
    966           schema.each_with_index do |table_and_columns, i| 
    967             table, columns = table_and_columns 
    968             columns.each_with_index { |column, j| schema_abbreviations["t#{i}_r#{j}"] = [ table, column ] } 
    969           end 
    970            
    971           return schema_abbreviations 
    972         end 
    973  
    974         def generate_primary_key_table(reflections, schema_abbreviations) 
    975           primary_key_lookup_table = {} 
    976           primary_key_lookup_table[table_name] =  
    977             schema_abbreviations.find { |cn, tc| tc == [ table_name, primary_key ] }.first 
    978  
    979           reflections.collect do |reflection|  
    980             primary_key_lookup_table[reflection.klass.table_name] = schema_abbreviations.find { |cn, tc|  
    981               tc == [ reflection.klass.table_name, reflection.klass.primary_key ] 
    982             }.first 
    983           end 
    984            
    985           return primary_key_lookup_table 
    986         end 
    987  
    988  
    989         def select_all_rows(options, schema_abbreviations, reflections) 
     915        def select_all_rows(options, join_dependency) 
    990916          connection.select_all( 
    991             construct_finder_sql_with_included_associations(options, schema_abbreviations, reflections),  
     917            construct_finder_sql_with_included_associations(options, join_dependency), 
    992918            "#{name} Load Including Associations" 
    993919          ) 
    994920        end 
    995921         
    996         def construct_counter_sql_with_included_associations(options, reflections
     922        def construct_counter_sql_with_included_associations(options, join_dependency
    997923          sql = "SELECT COUNT(DISTINCT #{table_name}.#{primary_key})" 
    998924           
     
    1003929           
    1004930          sql << " FROM #{table_name} " 
    1005           sql << reflections.collect { |reflection| association_join(reflection) }.to_s 
     931          sql << join_dependency.join_associations.collect{|join| join.association_join }.join 
    1006932          sql << "#{options[:joins]} " if options[:joins] 
    1007933 
    1008934          add_conditions!(sql, options[:conditions]) 
    1009           add_sti_conditions!(sql, reflections
    1010           add_limited_ids_condition!(sql, options, reflections) if !using_limitable_reflections?(reflections) && options[:limit] 
    1011  
    1012           add_limit!(sql, options) if using_limitable_reflections?(reflections) 
     935          add_sti_conditions!(sql, join_dependency
     936          add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit] 
     937 
     938          add_limit!(sql, options) if using_limitable_reflections?(join_dependency.reflections) 
    1013939 
    1014940          if !Base.connection.supports_count_distinct? 
     
    1019945        end 
    1020946 
    1021         def construct_finder_sql_with_included_associations(options, schema_abbreviations, reflections
    1022           sql = "SELECT #{column_aliases(schema_abbreviations)} FROM #{options[:from] || table_name} " 
    1023           sql << reflections.collect { |reflection| association_join(reflection) }.to_s 
     947        def construct_finder_sql_with_included_associations(options, join_dependency
     948          sql = "SELECT #{column_aliases(join_dependency)} FROM #{options[:from] || table_name} " 
     949          sql << join_dependency.join_associations.collect{|join| join.association_join }.join 
    1024950          sql << "#{options[:joins]} " if options[:joins] 
    1025951  
    1026952          add_conditions!(sql, options[:conditions]) 
    1027           add_sti_conditions!(sql, reflections
    1028           add_limited_ids_condition!(sql, options, reflections) if !using_limitable_reflections?(reflections) && options[:limit] 
     953          add_sti_conditions!(sql, join_dependency
     954          add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit] 
    1029955 
    1030956          sql << "ORDER BY #{options[:order]} " if options[:order] 
    1031957  
    1032           add_limit!(sql, options) if using_limitable_reflections?(reflections) 
     958          add_limit!(sql, options) if using_limitable_reflections?(join_dependency.reflections) 
    1033959  
    1034960          return sanitize_sql(sql) 
    1035961        end 
    1036962  
    1037         def add_limited_ids_condition!(sql, options, reflections
    1038           unless (id_list = select_limited_ids_list(options, reflections)).empty? 
     963        def add_limited_ids_condition!(sql, options, join_dependency
     964          unless (id_list = select_limited_ids_list(options, join_dependency)).empty? 
    1039965            sql << "#{condition_word(sql)} #{table_name}.#{primary_key} IN (#{id_list}) " 
    1040966          end 
    1041967        end 
    1042968  
    1043         def select_limited_ids_list(options, reflections
     969        def select_limited_ids_list(options, join_dependency
    1044970          connection.select_values( 
    1045             construct_finder_sql_for_association_limiting(options, reflections), 
     971            construct_finder_sql_for_association_limiting(options, join_dependency), 
    1046972            "#{name} Load IDs For Limited Eager Loading" 
    1047973          ).collect { |id| connection.quote(id) }.join(", ") 
    1048974        end 
    1049975  
    1050         def construct_finder_sql_for_association_limiting(options, reflections
     976        def construct_finder_sql_for_association_limiting(options, join_dependency
    1051977          #sql = "SELECT DISTINCT #{table_name}.#{primary_key} FROM #{table_name} " 
    1052978          sql = "SELECT " 
     
    1055981           
    1056982          if include_eager_conditions?(options) || include_eager_order?(options) 
    1057             sql << reflections.collect { |reflection| association_join(reflection) }.to_s 
     983            sql << join_dependency.join_associations.collect{|join| join.association_join }.join 
    1058984            sql << "#{options[:joins]} " if options[:joins] 
    1059985          end 
     
    10861012        end 
    10871013 
    1088         def add_sti_conditions!(sql, reflections) 
     1014        def join_depended_type_condition (klass, join_dependency) 
     1015          aliased_table_name = join_dependency.aliased_table_names_for(klass.table_name).last || klass.table_name 
     1016          quoted_inheritance_column = connection.quote_column_name(klass.inheritance_column) 
     1017          type_condition = klass.subclasses.inject(sti_condition(klass, aliased_table_name, quoted_inheritance_column)) do |condition, subclass| 
     1018            condition << " OR #{sti_condition subclass, aliased_table_name, quoted_inheritance_column}" 
     1019          end 
     1020         
     1021          " (#{type_condition}) " 
     1022        end 
     1023         
     1024        def sti_condition(klass, table_name, inheritance_column) 
     1025          "(#{table_name}.#{inheritance_column} = '#{klass.name.demodulize}' OR #{table_name}.#{inheritance_column} IS NULL)" 
     1026        end 
     1027 
     1028        #def join_depended_type_condition (klass, join_dependency) 
     1029        #  aliased_table_name = join_dependency.aliased_table_names_for(klass.table_name).first || klass.table_name 
     1030        #  quoted_inheritance_column = connection.quote_column_name(klass.inheritance_column) 
     1031        #  type_condition = klass.subclasses.inject("#{aliased_table_name}.#{quoted_inheritance_column} = '#{klass.name.demodulize}' ") do |condition, subclass| 
     1032        #    condition << "OR #{aliased_table_name}.#{quoted_inheritance_column} = '#{subclass.name.demodulize}' " 
     1033        #  end 
     1034        # 
     1035        #  " (#{type_condition}) " 
     1036        #end 
     1037 
     1038        def add_sti_conditions!(sql, join_dependency) 
     1039          reflections = join_dependency.reflections 
    10891040          sti_conditions = reflections.collect do |reflection| 
    1090             reflection.klass.send(:type_condition) unless reflection.klass.descends_from_active_record? 
     1041            join_depended_type_condition(reflection.klass, join_dependency) unless reflection.klass.descends_from_active_record? 
    10911042          end.compact 
    10921043           
     
    10961047        end 
    10971048 
    1098         def column_aliases(schema_abbreviations) 
    1099           schema_abbreviations.collect { |cn, tc| "#{tc[0]}.#{connection.quote_column_name tc[1]} AS #{cn}" }.join(", ") 
    1100         end 
    1101  
    1102         def association_join(reflection) 
    1103           case reflection.macro 
    1104             when :has_and_belongs_to_many 
    1105               " LEFT OUTER JOIN #{reflection.options[:join_table]} ON " + 
    1106               "#{reflection.options[:join_table]}.#{reflection.options[:foreign_key] || table_name.classify.foreign_key} = " + 
    1107               "#{table_name}.#{primary_key} " + 
    1108               " LEFT OUTER JOIN #{reflection.klass.table_name} ON " + 
    1109               "#{reflection.options[:join_table]}.#{reflection.options[:association_foreign_key] || reflection.klass.table_name.classify.foreign_key} = " + 
    1110               "#{reflection.klass.table_name}.#{reflection.klass.primary_key} " 
    1111             when :has_many, :has_one 
    1112               " LEFT OUTER JOIN #{reflection.klass.table_name} ON " + 
    1113               "#{reflection.klass.table_name}.#{reflection.options[:foreign_key] || table_name.classify.foreign_key} = " + 
    1114               "#{table_name}.#{primary_key} " 
    1115             when :belongs_to 
    1116               " LEFT OUTER JOIN #{reflection.klass.table_name} ON " + 
    1117               "#{reflection.klass.table_name}.#{reflection.klass.primary_key} = " + 
    1118               "#{table_name}.#{reflection.options[:foreign_key] || reflection.klass.table_name.classify.foreign_key} " 
    1119             else 
    1120               "" 
    1121           end           
     1049        def column_aliases(join_dependency) 
     1050          join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name| 
     1051              "#{join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ") 
    11221052        end 
    11231053 
     
    11341064        end 
    11351065 
    1136         def extract_record(schema_abbreviations, table_name, row) 
    1137           record = {} 
    1138           row.each do |column, value| 
    1139             prefix, column_name = schema_abbreviations[column] 
    1140             record[column_name] = value if prefix == table_name 
    1141           end 
    1142           return record 
    1143         end         
    1144  
    11451066        def condition_word(sql) 
    11461067          sql =~ /where/i ? " AND " : "WHERE " 
     
    11551076           
    11561077          extension_module_name.constantize 
     1078        end 
     1079 
     1080        class JoinDependency 
     1081          attr_reader :joins, :reflections 
     1082 
     1083          def initialize(base, associations) 
     1084            @joins                 = [JoinBase.new(base)] 
     1085            @associations          = associations 
     1086            @reflections           = [] 
     1087            @base_records_hash     = {} 
     1088            @base_records_in_order = [] 
     1089            build(associations) 
     1090          end 
     1091 
     1092          def join_associations 
     1093            @joins[1..-1].to_a 
     1094          end 
     1095 
     1096          def join_base 
     1097            @joins[0] 
     1098          end 
     1099 
     1100          def instantiate(rows) 
     1101            rows.each_with_index do |row, i| 
     1102              primary_id = join_base.record_id(row) 
     1103              unless @base_records_hash[primary_id] 
     1104                @base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row)) 
     1105              end 
     1106              construct(@base_records_hash[primary_id], @associations, join_associations.dup, row) 
     1107            end 
     1108            return @base_records_in_order 
     1109          end 
     1110 
     1111          def aliased_table_names_for(table_name) 
     1112            joins.select{|join| join.table_name == table_name }.collect{|join| join.aliased_table_name} 
     1113          end 
     1114 
     1115          protected 
     1116            def build(associations, parent = nil) 
     1117              parent ||= @joins.last 
     1118              case associations 
     1119                when Symbol, String 
     1120                  reflection = parent.reflections[associations.to_s.intern] or 
     1121                  raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" 
     1122                  @reflections << reflection 
     1123                  @joins << JoinAssociation.new(reflection, self, parent) 
     1124                when Array 
     1125                  associations.each do |association| 
     1126                    build(association, parent) 
     1127                  end 
     1128                when Hash 
     1129                  associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| 
     1130                    build(name, parent) 
     1131                    build(associations[name]) 
     1132                  end 
     1133                else 
     1134                  raise ConfigurationError, associations.inspect 
     1135              end 
     1136            end 
     1137 
     1138            def construct(parent, associations, joins, row) 
     1139              case associations 
     1140                when Symbol, String 
     1141                  while (join = joins.shift).reflection.name.to_s != associations.to_s 
     1142                    raise ConfigurationError, "Not Enough Associations" if joins.empty? 
     1143                  end 
     1144                  construct_association(parent, join, row) 
     1145                when Array 
     1146                  associations.each do |association| 
     1147                    construct(parent, association, joins, row) 
     1148                  end 
     1149                when Hash 
     1150                  associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| 
     1151                    association = construct_association(parent, joins.shift, row) 
     1152                    construct(association, associations[name], joins, row) if association 
     1153                  end 
     1154                else 
     1155                  raise ConfigurationError, associations.inspect 
     1156              end 
     1157            end 
     1158 
     1159            def construct_association(record, join, row) 
     1160              case join.reflection.macro 
     1161                when :has_many, :has_and_belongs_to_many 
     1162                  collection = record.send(join.reflection.name) 
     1163                  collection.loaded 
     1164     
     1165                  return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil? 
     1166                  association = join.instantiate(row) 
     1167                  collection.target.push(association) unless collection.target.include?(association) 
     1168                when :has_one, :belongs_to 
     1169                  return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil? 
     1170                  association = join.instantiate(row) 
     1171                  record.send("set_#{join.reflection.name}_target", association) 
     1172                else 
     1173                  raise ConfigurationError, "unknown macro: #{join.reflection.macro}" 
     1174              end 
     1175              return association 
     1176            end 
     1177 
     1178          class JoinBase 
     1179            attr_reader :active_record 
     1180            delegate    :table_name, :column_names, :primary_key, :reflections, :to=>:active_record 
     1181 
     1182            def initialize(active_record) 
     1183              @active_record = active_record 
     1184              @cached_record = {} 
     1185            end 
     1186 
     1187            def aliased_prefix 
     1188              "t0" 
     1189            end 
     1190 
     1191            def aliased_primary_key 
     1192              "#{ aliased_prefix }_r0" 
     1193            end 
     1194 
     1195            def aliased_table_name 
     1196              active_record.table_name 
     1197            end 
     1198 
     1199            def column_names_with_alias 
     1200              unless @column_names_with_alias 
     1201                @column_names_with_alias = [] 
     1202                ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i| 
     1203                  @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"] 
     1204                end 
     1205              end 
     1206              return @column_names_with_alias 
     1207            end 
     1208 
     1209            def extract_record(row) 
     1210              column_names_with_alias.inject({}){|record, (cn, an)| record[cn] = row[an]; record} 
     1211            end 
     1212 
     1213            def record_id(row) 
     1214              row[aliased_primary_key] 
     1215            end 
     1216 
     1217            def instantiate(row) 
     1218              @cached_record[record_id(row)] ||= active_record.instantiate(extract_record(row)) 
     1219            end 
     1220          end 
     1221 
     1222          class JoinAssociation < JoinBase 
     1223            attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix 
     1224            delegate    :options, :klass, :to=>:reflection 
     1225 
     1226            def initialize(reflection, join_dependency, parent = nil) 
     1227              super(reflection.klass) 
     1228              @parent             = parent 
     1229              @reflection         = reflection 
     1230              @aliased_prefix     = "t#{ join_dependency.joins.size }" 
     1231              @aliased_table_name = join_dependency.aliased_table_names_for(table_name).empty? ? table_name : @aliased_prefix 
     1232            end 
     1233 
     1234            def association_join 
     1235              case reflection.macro 
     1236                when :has_and_belongs_to_many 
     1237                  join_table_name    = 
     1238                  " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [ 
     1239                     options[:join_table], options[:join_table], 
     1240                     options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key, 
     1241                     reflection.active_record.table_name, reflection.active_record.primary_key] + 
     1242                  " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [ 
     1243                     aliased_table_name, aliased_table_name, klass.primary_key, 
     1244                     options[:join_table], options[:association_foreign_key] || klass.table_name.classify.foreign_key 
     1245                     ] 
     1246                when :has_many, :has_one 
     1247                  " LEFT OUTER JOIN %s AS %s ON %s.%s = %s.%s " % [table_name, aliased_table_name, 
     1248                     aliased_table_name, options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key, 
     1249                     parent.aliased_table_name, parent.primary_key 
     1250                    ] 
     1251                when :belongs_to 
     1252                  " LEFT OUTER JOIN %s AS %s ON %s.%s = %s.%s " % [table_name, aliased_table_name, 
     1253                     aliased_table_name, reflection.klass.primary_key, 
     1254                     parent.aliased_table_name, options[:foreign_key] || reflection.klass.to_s.classify.foreign_key 
     1255                    ] 
     1256                else 
     1257                  "" 
     1258              end 
     1259            end 
     1260          end 
    11571261        end 
    11581262    end 
  • trunk/activerecord/test/associations_go_eager_test.rb

    r3566 r3769  
    9393 
    9494  def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations 
    95     posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1) 
     95    posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :order => 'posts.id') 
     96    assert_equal 1, posts.length 
     97    assert_equal [3], posts.collect { |p| p.id } 
     98  end 
     99   
     100  def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations 
     101    posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id') 
    96102    assert_equal 1, posts.length 
    97103    assert_equal [4], posts.collect { |p| p.id } 
    98   end 
    99    
    100   def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations 
    101     posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :offset => 1) 
    102     assert_equal 0, posts.length 
    103     assert_equal [], posts 
    104104  end 
    105105