Files

Class Index [+]

Quicksearch

ActiveRecord::AssociationPreload::ClassMethods

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.

Protected Instance Methods

preload_associations(records, associations, preload_options={}) click to toggle source

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.

Parameters

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

Private Instance Methods

add_preloaded_record_to_collection(parent_records, reflection_name, associated_record) click to toggle source
     # 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
add_preloaded_records_to_collection(parent_records, reflection_name, associated_record) click to toggle source
     # 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
append_conditions(reflection, preload_options) click to toggle source
     # 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
construct_id_map(records, primary_key=nil) click to toggle source

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
find_associated_records(ids, reflection, preload_options) click to toggle source
     # 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
in_or_equals_for_ids(ids) click to toggle source
     # File lib/active_record/association_preload.rb, line 398
398:       def in_or_equals_for_ids(ids)
399:         ids.size > 1 ? "IN (?)" : "= ?"
400:       end
interpolate_sql_for_preload(sql) click to toggle source
     # 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
preload_belongs_to_association(records, reflection, preload_options={}) click to toggle source
     # 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
preload_has_and_belongs_to_many_association(records, reflection, preload_options={}) click to toggle source
     # 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
preload_has_many_association(records, reflection, preload_options={}) click to toggle source
     # 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
preload_has_one_association(records, reflection, preload_options={}) click to toggle source
     # 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
preload_one_association(records, association, preload_options={}) click to toggle source

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
preload_through_records(records, reflection, through_association) click to toggle source
     # 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] || reflection.options[:order]
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
set_association_collection_records(id_to_record_map, reflection_name, associated_records, key) click to toggle source
     # 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
set_association_single_records(id_to_record_map, reflection_name, associated_records, key) click to toggle source
     # 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.

[Validate]

Generated with the Darkfish Rdoc Generator 1.1.6.