1 module CompositePrimaryKeys
3 module AssociationPreload
4 def self.append_features(base)
6 base.send(:extend, ClassMethods)
9 # Composite key versions of Association functions
11 def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
12 table_name = reflection.klass.quoted_table_name
13 id_to_record_map, ids = construct_id_map_for_composite(records)
14 records.each {|record| record.send(reflection.name).loaded}
15 options = reflection.options
18 primary_key = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP)
19 where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys|
20 "(" + keys.map{|key| "t0.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
23 conditions = [where, ids].flatten
24 joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{full_composite_join_clause(reflection, reflection.klass.table_name, reflection.klass.primary_key, 't0', reflection.association_foreign_key)}"
25 parent_primary_keys = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| "t0.#{connection.quote_column_name(k)}"}
26 parent_record_id = connection.concat(*parent_primary_keys.zip(["','"] * (parent_primary_keys.size - 1)).flatten.compact)
28 conditions = ["t0.#{connection.quote_column_name(reflection.primary_key_name)} IN (?)", ids]
29 joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{connection.quote_column_name(reflection.klass.primary_key)} = t0.#{connection.quote_column_name(reflection.association_foreign_key)})"
30 parent_record_id = reflection.primary_key_name
33 conditions.first << append_conditions(reflection, preload_options)
35 associated_records = reflection.klass.find(:all,
36 :conditions => conditions,
37 :include => options[:include],
39 :select => "#{options[:select] || table_name+'.*'}, #{parent_record_id} as parent_record_id_",
40 :order => options[:order])
42 set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'parent_record_id_')
45 def preload_has_many_association(records, reflection, preload_options={})
46 id_to_record_map, ids = construct_id_map_for_composite(records)
47 records.each {|record| record.send(reflection.name).loaded}
48 options = reflection.options
51 through_records = preload_through_records(records, reflection, options[:through])
52 through_reflection = reflections[options[:through]]
53 through_primary_key = through_reflection.primary_key_name
55 unless through_records.empty?
56 source = reflection.source_reflection.name
57 #add conditions from reflection!
58 through_records.first.class.preload_associations(through_records, source, reflection.options)
59 through_records.each do |through_record|
60 key = through_primary_key.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| through_record.send(k)}.join(CompositePrimaryKeys::ID_SEP)
61 add_preloaded_records_to_collection(id_to_record_map[key], reflection.name, through_record.send(source))
65 associated_records = find_associated_records(ids, reflection, preload_options)
66 set_association_collection_records(id_to_record_map, reflection.name, associated_records, reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP))
70 def preload_through_records(records, reflection, through_association)
71 through_reflection = reflections[through_association]
72 through_primary_key = through_reflection.primary_key_name
74 if reflection.options[:source_type]
75 interface = reflection.source_reflection.options[:foreign_type]
76 preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
79 records.first.class.preload_associations(records, through_association, preload_options)
81 # Dont cache the association - we would only be caching a subset
83 records.each do |record|
84 proxy = record.send(through_association)
86 if proxy.respond_to?(:target)
87 through_records << proxy.target
89 else # this is a has_one :through reflection
90 through_records << proxy if proxy
93 through_records.flatten!
95 records.first.class.preload_associations(records, through_association)
96 through_records = records.map {|record| record.send(through_association)}.flatten
99 through_records.compact!
103 def preload_belongs_to_association(records, reflection, preload_options={})
104 options = reflection.options
105 primary_key_name = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP)
107 if options[:polymorphic]
108 raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
110 # I need to keep the original ids for each record (as opposed to the stringified) so
111 # that they get properly converted for each db so the id_map ends up looking like:
113 # { '1,2' => {:id => [1,2], :records => [...records...]}}
116 records.each do |record|
117 key = primary_key_name.map{|k| record.attributes[k]}
118 key_as_string = key.join(CompositePrimaryKeys::ID_SEP)
121 mapped_records = (id_map[key_as_string] ||= {:id => key, :records => []})
122 mapped_records[:records] << record
127 klasses_and_ids = [[reflection.klass.name, id_map]]
130 klasses_and_ids.each do |klass_and_id|
131 klass_name, id_map = *klass_and_id
132 klass = klass_name.constantize
133 table_name = klass.quoted_table_name
134 connection = reflection.active_record.connection
137 primary_key = klass.primary_key.to_s.split(CompositePrimaryKeys::ID_SEP)
138 ids = id_map.keys.uniq.map {|id| id_map[id][:id]}
140 where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys|
141 "(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
144 conditions = [where, ids].flatten
146 conditions = ["#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)", id_map.keys.uniq]
149 conditions.first << append_conditions(reflection, preload_options)
151 associated_records = klass.find(:all,
152 :conditions => conditions,
153 :include => options[:include],
154 :select => options[:select],
155 :joins => options[:joins],
156 :order => options[:order])
158 set_association_single_records(id_map, reflection.name, associated_records, primary_key)
162 def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
163 associated_records.each do |associated_record|
164 associated_record_key = associated_record[key]
165 associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s
166 mapped_records = id_to_record_map[associated_record_key]
167 add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
171 def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
173 associated_records.each do |associated_record|
174 associated_record_key = associated_record[key]
175 associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s
177 #this is a has_one or belongs_to: there should only be one record.
178 #Unfortunately we can't (in portable way) ask the database for 'all records where foo_id in (x,y,z), but please
179 # only one row per distinct foo_id' so this where we enforce that
180 next if seen_keys[associated_record_key]
181 seen_keys[associated_record_key] = true
182 mapped_records = id_to_record_map[associated_record_key][:records]
183 mapped_records.each do |mapped_record|
184 mapped_record.send("set_#{reflection_name}_target", associated_record)
189 def find_associated_records(ids, reflection, preload_options)
190 options = reflection.options
191 table_name = reflection.klass.quoted_table_name
193 if interface = reflection.options[:as]
194 raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
196 connection = reflection.active_record.connection
197 foreign_key = reflection.primary_key_name
198 conditions = ["#{table_name}.#{connection.quote_column_name(foreign_key)} IN (?)", ids]
201 foreign_keys = foreign_key.to_s.split(CompositePrimaryKeys::ID_SEP)
203 where = (foreign_keys * ids.size).in_groups_of(foreign_keys.size).map do |keys|
204 "(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
207 conditions = [where, ids].flatten
211 conditions.first << append_conditions(reflection, preload_options)
213 reflection.klass.find(:all,
214 :select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
215 :include => preload_options[:include] || options[:include],
216 :conditions => conditions,
217 :joins => options[:joins],
218 :group => preload_options[:group] || options[:group],
219 :order => preload_options[:order] || options[:order])
222 # Given a collection of ActiveRecord objects, constructs a Hash which maps
223 # the objects' IDs to the relevant objects. Returns a 2-tuple
224 # <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash,
225 # and +ids+ is an Array of record IDs.
226 def construct_id_map_for_composite(records)
227 id_to_record_map = {}
229 records.each do |record|
230 primary_key ||= record.class.primary_key
232 mapped_records = (id_to_record_map[record.id.to_s] ||= [])
233 mapped_records << record
236 return id_to_record_map, ids
239 def full_composite_join_clause(reflection, table1, full_keys1, table2, full_keys2)
240 connection = reflection.active_record.connection
241 full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
242 full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
243 where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
244 quoted1 = connection.quote_table_name(table1)
245 quoted2 = connection.quote_table_name(table2)
246 "#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}"