1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
|
module ActiveRecord
module AssociationPreload #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
# Loads the named associations for the activerecord record (or records) given
# preload_options is passed only one level deep: don't pass to the child associations when associations is a Hash
protected
def preload_associations(records, associations, preload_options={})
records = [records].flatten.compact.uniq
return if records.empty?
case associations
when Array then associations.each {|association| preload_associations(records, association, preload_options)}
when Symbol, String then preload_one_association(records, associations.to_sym, preload_options)
when Hash then
associations.each do |parent, child|
raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol)
preload_associations(records, parent, preload_options)
reflection = reflections[parent]
parents = records.map {|record| record.send(reflection.name)}.flatten
unless parents.empty? || parents.first.nil?
parents.first.class.preload_associations(parents, child)
end
end
end
end
private
def preload_one_association(records, association, preload_options={})
class_to_reflection = {}
# Not all records have the same class, so group then preload
# group on the reflection itself so that if various subclass share the same association then we do not split them
# unncessarily
records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
send("preload_#{reflection.macro}_association", records, reflection, preload_options)
end
end
def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
association_proxy = parent_record.send(reflection_name)
association_proxy.loaded
association_proxy.target.push(*[associated_record].flatten)
end
end
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
association_proxy = parent_record.send(reflection_name)
association_proxy.loaded
association_proxy.target = associated_record
end
end
def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
associated_records.each do |associated_record|
mapped_records = id_to_record_map[associated_record[key].to_s]
add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
end
end
def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
seen_keys = {}
associated_records.each do |associated_record|
#this is a has_one or belongs_to: there should only be one record.
#Unfortunately we can't (in portable way) ask the database for 'all records where foo_id in (x,y,z), but please
# only one row per distinct foo_id' so this where we enforce that
next if seen_keys[associated_record[key].to_s]
seen_keys[associated_record[key].to_s] = true
mapped_records = id_to_record_map[associated_record[key].to_s]
mapped_records.each do |mapped_record|
mapped_record.send("set_#{reflection_name}_target", associated_record)
end
end
end
def construct_id_map(records)
id_to_record_map = {}
ids = []
records.each do |record|
ids << record.id
mapped_records = (id_to_record_map[record.id.to_s] ||= [])
mapped_records << record
end
ids.uniq!
return id_to_record_map, ids
end
def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
table_name = reflection.klass.quoted_table_name
id_to_record_map, ids = construct_id_map(records)
records.each {|record| record.send(reflection.name).loaded}
options = reflection.options
conditions = "t0.#{reflection.primary_key_name} IN (?)"
conditions << append_conditions(options, preload_options)
associated_records = reflection.klass.find(:all, :conditions => [conditions, ids],
:include => options[:include],
:joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} as t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as _parent_record_id",
:order => options[:order])
set_association_collection_records(id_to_record_map, reflection.name, associated_records, '_parent_record_id')
end
def preload_has_one_association(records, reflection, preload_options={})
id_to_record_map, ids = construct_id_map(records)
options = reflection.options
if options[:through]
records.each {|record| record.send(reflection.name) && record.send(reflection.name).loaded}
through_records = preload_through_records(records, reflection, options[:through])
through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name
unless through_records.empty?
source = reflection.source_reflection.name
through_records.first.class.preload_associations(through_records, source)
through_records.each do |through_record|
add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
reflection.name, through_record.send(source))
end
end
else
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
end
end
def preload_has_many_association(records, reflection, preload_options={})
id_to_record_map, ids = construct_id_map(records)
records.each {|record| record.send(reflection.name).loaded}
options = reflection.options
if options[:through]
through_records = preload_through_records(records, reflection, options[:through])
through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name
unless through_records.empty?
source = reflection.source_reflection.name
#add conditions from reflection!
through_records.first.class.preload_associations(through_records, source, reflection.options)
through_records.each do |through_record|
add_preloaded_records_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
reflection.name, through_record.send(source))
end
end
else
set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
reflection.primary_key_name)
end
end
def preload_through_records(records, reflection, through_association)
through_reflection = reflections[through_association]
through_primary_key = through_reflection.primary_key_name
if reflection.options[:source_type]
interface = reflection.source_reflection.options[:foreign_type]
preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
records.compact!
records.first.class.preload_associations(records, through_association, preload_options)
# Dont cache the association - we would only be caching a subset
through_records = []
records.each do |record|
proxy = record.send(through_association)
if proxy.respond_to?(:target)
through_records << proxy.target
proxy.reset
else # this is a has_one :through reflection
through_records << proxy if proxy
end
end
through_records.flatten!
else
records.first.class.preload_associations(records, through_association)
through_records = records.map {|record| record.send(through_association)}.flatten
end
through_records.compact!
through_records
end
# FIXME: quoting
def preload_belongs_to_association(records, reflection, preload_options={})
options = reflection.options
primary_key_name = reflection.primary_key_name
if options[:polymorphic]
polymorph_type = options[:foreign_type]
klasses_and_ids = {}
# Construct a mapping from klass to a list of ids to load and a mapping of those ids back to their parent_records
records.each do |record|
if klass = record.send(polymorph_type)
klass_id = record.send(primary_key_name)
if klass_id
id_map = klasses_and_ids[klass] ||= {}
id_list_for_klass_id = (id_map[klass_id.to_s] ||= [])
id_list_for_klass_id << record
end
end
end
klasses_and_ids = klasses_and_ids.to_a
else
id_map = {}
records.each do |record|
key = record.send(primary_key_name)
if key
mapped_records = (id_map[key.to_s] ||= [])
mapped_records << record
end
end
klasses_and_ids = [[reflection.klass.name, id_map]]
end
klasses_and_ids.each do |klass_and_id|
klass_name, id_map = *klass_and_id
klass = klass_name.constantize
table_name = klass.quoted_table_name
primary_key = klass.primary_key
conditions = "#{table_name}.#{primary_key} IN (?)"
conditions << append_conditions(options, preload_options)
associated_records = klass.find(:all, :conditions => [conditions, id_map.keys.uniq],
:include => options[:include],
:select => options[:select],
:joins => options[:joins],
:order => options[:order])
set_association_single_records(id_map, reflection.name, associated_records, primary_key)
end
end
def find_associated_records(ids, reflection, preload_options)
options = reflection.options
table_name = reflection.klass.quoted_table_name
if interface = reflection.options[:as]
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.name.demodulize}'"
else
foreign_key = reflection.primary_key_name
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)"
end
conditions << append_conditions(options, preload_options)
reflection.klass.find(:all,
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
:include => preload_options[:include] || options[:include],
:conditions => [conditions, ids],
:joins => options[:joins],
:group => preload_options[:group] || options[:group],
:order => preload_options[:order] || options[:order])
end
def interpolate_sql_for_preload(sql)
instance_eval("%@#{sql.gsub('@', '\@')}@")
end
def append_conditions(options, preload_options)
sql = ""
sql << " AND (#{interpolate_sql_for_preload(sanitize_sql(options[:conditions]))})" if options[:conditions]
sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]
sql
end
end
end
end
|