Implements the details of eager loading of Active Record associations. Application developers should not use this module directly.
ActiveRecord::Base is extended with this module. The source code in ActiveRecord::Base references methods defined in this module.
Note that ‘eager loading’ and ‘preloading’ are actually the same thing. However, there are two different eager loading strategies.
The first one is by using table joins. This was only strategy available prior to Rails 2.1. Suppose that you have an Author model with columns ‘name’ and ‘age’, and a Book model with columns ‘name’ and ‘sales’. Using this strategy, Active Record would try to retrieve all data for an author and all of its books via a single query:
SELECT * FROM authors LEFT OUTER JOIN books ON authors.id = books.id WHERE authors.name = 'Ken Akamatsu'
However, this could result in many rows that contain redundant data. After having received the first row, we already have enough data to instantiate the Author object. In all subsequent rows, only the data for the joined ‘books’ table is useful; the joined ‘authors’ data is just redundant, and processing this redundant data takes memory and CPU time. The problem quickly becomes worse and worse as the level of eager loading increases (i.e. if Active Record is to eager load the associations’ associations as well).
The second strategy is to use multiple database queries, one for each level of association. Since Rails 2.1, this is the default strategy. In situations where a table join is necessary (e.g. when the :conditions option references an association’s column), it will fallback to the table join strategy.
See also ActiveRecord::Associations::ClassMethods, which explains eager loading in a more high-level (application developer-friendly) manner.
Eager loads the named associations for the given Active Record record(s).
In this description, ‘association name’ shall refer to the name passed to an association creation method. For example, a model that specifies belongs_to :author, has_many :buyers has association names :author and :buyers.
records is an array of ActiveRecord::Base. This array needs not be flat, i.e. records itself may also contain arrays of records. In any case, preload_associations will preload the all associations records by flattening records.
associations specifies one or more associations that you want to preload. It may be:
a Symbol or a String which specifies a single association name. For example, specifying :books allows this method to preload all books for an Author.
an Array which specifies multiple association names. This array is processed recursively. For example, specifying [:avatar, :books] allows this method to preload an author’s avatar as well as all of his books.
a Hash which specifies multiple association names, as well as association names for the to-be-preloaded association objects. For example, specifying { :author => :avatar } will preload a book’s author, as well as that author’s avatar.
:associations has the same format as the :include option for ActiveRecord::Base.find. So associations could look like this:
:books [ :books, :author ] { :author => :avatar } [ :books, { :author => :avatar } ]
preload_options contains options that will be passed to ActiveRecord::Base#find (which is called under the hood for preloading records). But it is passed only one level deep in the associations argument, i.e. it’s not passed to the child associations when associations is a Hash.
# File lib/active_record/association_preload.rb, line 87 87: def preload_associations(records, associations, preload_options={}) 88: records = Array.wrap(records).compact.uniq 89: return if records.empty? 90: case associations 91: when Array then associations.each {|association| preload_associations(records, association, preload_options)} 92: when Symbol, String then preload_one_association(records, associations.to_sym, preload_options) 93: when Hash then 94: associations.each do |parent, child| 95: raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol) 96: preload_associations(records, parent, preload_options) 97: reflection = reflections[parent] 98: parents = records.sum { |record| Array.wrap(record.send(reflection.name)) } 99: unless parents.empty? 100: parents.first.class.preload_associations(parents, child) 101: end 102: end 103: end 104: end
# File lib/active_record/association_preload.rb, line 135 135: def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record) 136: parent_records.each do |parent_record| 137: parent_record.send("set_#{reflection_name}_target", associated_record) 138: end 139: end
# File lib/active_record/association_preload.rb, line 125 125: def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record) 126: parent_records.each do |parent_record| 127: association_proxy = parent_record.send(reflection_name) 128: association_proxy.loaded 129: association_proxy.target.push(*Array.wrap(associated_record)) 130: 131: association_proxy.__send__(:set_inverse_instance, associated_record, parent_record) 132: end 133: end
# File lib/active_record/association_preload.rb, line 391 391: def append_conditions(reflection, preload_options) 392: sql = "" 393: sql << " AND (#{interpolate_sql_for_preload(reflection.sanitized_conditions)})" if reflection.sanitized_conditions 394: sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions] 395: sql 396: end
Given a collection of Active Record objects, constructs a Hash which maps the objects’ IDs to the relevant objects. Returns a 2-tuple (id_to_record_map, ids) where id_to_record_map is the Hash, and ids is an Array of record IDs.
# File lib/active_record/association_preload.rb, line 174 174: def construct_id_map(records, primary_key=nil) 175: id_to_record_map = {} 176: ids = [] 177: records.each do |record| 178: primary_key ||= record.class.primary_key 179: ids << record[primary_key] 180: mapped_records = (id_to_record_map[ids.last.to_s] ||= []) 181: mapped_records << record 182: end 183: ids.uniq! 184: return id_to_record_map, ids 185: end
# File lib/active_record/association_preload.rb, line 361 361: def find_associated_records(ids, reflection, preload_options) 362: options = reflection.options 363: table_name = reflection.klass.quoted_table_name 364: 365: if interface = reflection.options[:as] 366: conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'" 367: else 368: foreign_key = reflection.primary_key_name 369: conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}" 370: end 371: 372: conditions << append_conditions(reflection, preload_options) 373: 374: find_options = { 375: :select => preload_options[:select] || options[:select] || Arel::SqlLiteral.new("#{table_name}.*"), 376: :include => preload_options[:include] || options[:include], 377: :conditions => [conditions, ids], 378: :joins => options[:joins], 379: :group => preload_options[:group] || options[:group], 380: :order => preload_options[:order] || options[:order] 381: } 382: 383: reflection.klass.scoped.apply_finder_options(find_options).to_a 384: end
# File lib/active_record/association_preload.rb, line 398 398: def in_or_equals_for_ids(ids) 399: ids.size > 1 ? "IN (?)" : "= ?" 400: end
# File lib/active_record/association_preload.rb, line 387 387: def interpolate_sql_for_preload(sql) 388: instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) 389: end
# File lib/active_record/association_preload.rb, line 299 299: def preload_belongs_to_association(records, reflection, preload_options={}) 300: return if records.first.send("loaded_#{reflection.name}?") 301: options = reflection.options 302: primary_key_name = reflection.primary_key_name 303: 304: if options[:polymorphic] 305: polymorph_type = options[:foreign_type] 306: klasses_and_ids = {} 307: 308: # Construct a mapping from klass to a list of ids to load and a mapping of those ids back 309: # to their parent_records 310: records.each do |record| 311: if klass = record.send(polymorph_type) 312: klass_id = record.send(primary_key_name) 313: if klass_id 314: id_map = klasses_and_ids[klass] ||= {} 315: id_list_for_klass_id = (id_map[klass_id.to_s] ||= []) 316: id_list_for_klass_id << record 317: end 318: end 319: end 320: klasses_and_ids = klasses_and_ids.to_a 321: else 322: id_map = {} 323: records.each do |record| 324: key = record.send(primary_key_name) 325: if key 326: mapped_records = (id_map[key.to_s] ||= []) 327: mapped_records << record 328: end 329: end 330: klasses_and_ids = [[reflection.klass.name, id_map]] 331: end 332: 333: klasses_and_ids.each do |klass_and_id| 334: klass_name, id_map = *klass_and_id 335: next if id_map.empty? 336: klass = klass_name.constantize 337: 338: table_name = klass.quoted_table_name 339: primary_key = reflection.options[:primary_key] || klass.primary_key 340: column_type = klass.columns.detect{|c| c.name == primary_key}.type 341: 342: ids = id_map.keys.map do |id| 343: if column_type == :integer 344: id.to_i 345: elsif column_type == :float 346: id.to_f 347: else 348: id 349: end 350: end 351: 352: conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}" 353: conditions << append_conditions(reflection, preload_options) 354: 355: associated_records = klass.unscoped.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a 356: 357: set_association_single_records(id_map, reflection.name, associated_records, primary_key) 358: end 359: end
# File lib/active_record/association_preload.rb, line 187 187: def preload_has_and_belongs_to_many_association(records, reflection, preload_options={}) 188: table_name = reflection.klass.quoted_table_name 189: id_to_record_map, ids = construct_id_map(records) 190: records.each {|record| record.send(reflection.name).loaded} 191: options = reflection.options 192: 193: conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}" 194: conditions << append_conditions(reflection, preload_options) 195: 196: associated_records = reflection.klass.unscoped.where([conditions, ids]). 197: includes(options[:include]). 198: joins("INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}"). 199: select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id"). 200: order(options[:order]).to_a 201: 202: set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id') 203: end
# File lib/active_record/association_preload.rb, line 236 236: def preload_has_many_association(records, reflection, preload_options={}) 237: return if records.first.send(reflection.name).loaded? 238: options = reflection.options 239: 240: primary_key_name = reflection.through_reflection_primary_key_name 241: id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key]) 242: records.each {|record| record.send(reflection.name).loaded} 243: 244: if options[:through] 245: through_records = preload_through_records(records, reflection, options[:through]) 246: through_reflection = reflections[options[:through]] 247: unless through_records.empty? 248: source = reflection.source_reflection.name 249: through_records.first.class.preload_associations(through_records, source, options) 250: through_records.each do |through_record| 251: through_record_id = through_record[reflection.through_reflection_primary_key].to_s 252: add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source)) 253: end 254: end 255: 256: else 257: set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), 258: reflection.primary_key_name) 259: end 260: end
# File lib/active_record/association_preload.rb, line 205 205: def preload_has_one_association(records, reflection, preload_options={}) 206: return if records.first.send("loaded_#{reflection.name}?") 207: id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key]) 208: options = reflection.options 209: records.each {|record| record.send("set_#{reflection.name}_target", nil)} 210: if options[:through] 211: through_records = preload_through_records(records, reflection, options[:through]) 212: through_reflection = reflections[options[:through]] 213: through_primary_key = through_reflection.primary_key_name 214: unless through_records.empty? 215: source = reflection.source_reflection.name 216: through_records.first.class.preload_associations(through_records, source) 217: if through_reflection.macro == :belongs_to 218: rev_id_to_record_map, rev_ids = construct_id_map(records, through_primary_key) 219: rev_primary_key = through_reflection.klass.primary_key 220: through_records.each do |through_record| 221: add_preloaded_record_to_collection(rev_id_to_record_map[through_record[rev_primary_key].to_s], 222: reflection.name, through_record.send(source)) 223: end 224: else 225: through_records.each do |through_record| 226: add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s], 227: reflection.name, through_record.send(source)) 228: end 229: end 230: end 231: else 232: set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name) 233: end 234: end
Preloads a specific named association for the given records. This is called by preload_associations as its base case.
# File lib/active_record/association_preload.rb, line 110 110: def preload_one_association(records, association, preload_options={}) 111: class_to_reflection = {} 112: # Not all records have the same class, so group then preload 113: # group on the reflection itself so that if various subclass share the same association then 114: # we do not split them unnecessarily 115: records.group_by { |record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, _records| 116: raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection 117: 118: # 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus, 119: # the following could call 'preload_belongs_to_association', 120: # 'preload_has_many_association', etc. 121: send("preload_#{reflection.macro}_association", _records, reflection, preload_options) 122: end 123: end
# File lib/active_record/association_preload.rb, line 262 262: def preload_through_records(records, reflection, through_association) 263: through_reflection = reflections[through_association] 264: through_primary_key = through_reflection.primary_key_name 265: 266: through_records = [] 267: if reflection.options[:source_type] 268: interface = reflection.source_reflection.options[:foreign_type] 269: preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]} 270: 271: records.compact! 272: records.first.class.preload_associations(records, through_association, preload_options) 273: 274: # Dont cache the association - we would only be caching a subset 275: records.each do |record| 276: proxy = record.send(through_association) 277: 278: if proxy.respond_to?(:target) 279: through_records.concat Array.wrap(proxy.target) 280: proxy.reset 281: else # this is a has_one :through reflection 282: through_records << proxy if proxy 283: end 284: end 285: else 286: options = {} 287: options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions] 288: options[:order] = reflection.options[:order] 289: options[:conditions] = reflection.options[:conditions] 290: records.first.class.preload_associations(records, through_association, options) 291: 292: records.each do |record| 293: through_records.concat Array.wrap(record.send(through_association)) 294: end 295: end 296: through_records 297: end
# File lib/active_record/association_preload.rb, line 141 141: def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key) 142: associated_records.each do |associated_record| 143: mapped_records = id_to_record_map[associated_record[key].to_s] 144: add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record) 145: end 146: end
# File lib/active_record/association_preload.rb, line 148 148: def set_association_single_records(id_to_record_map, reflection_name, associated_records, key) 149: seen_keys = {} 150: associated_records.each do |associated_record| 151: #this is a has_one or belongs_to: there should only be one record. 152: #Unfortunately we can't (in portable way) ask the database for 153: #'all records where foo_id in (x,y,z), but please 154: # only one row per distinct foo_id' so this where we enforce that 155: next if seen_keys[associated_record[key].to_s] 156: seen_keys[associated_record[key].to_s] = true 157: mapped_records = id_to_record_map[associated_record[key].to_s] 158: mapped_records.each do |mapped_record| 159: association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record) 160: association_proxy.__send__(:set_inverse_instance, associated_record, mapped_record) 161: end 162: end 163: 164: id_to_record_map.each do |id, records| 165: next if seen_keys.include?(id.to_s) 166: records.each {|record| record.send("set_#{reflection_name}_target", nil) } 167: end 168: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.