]> git.openstreetmap.org Git - rails.git/blob - vendor/gems/composite_primary_keys-2.2.2/lib/composite_primary_keys/association_preload.rb
Add version 2.2.2 of composite_primary_keys.
[rails.git] / vendor / gems / composite_primary_keys-2.2.2 / lib / composite_primary_keys / association_preload.rb
1 module CompositePrimaryKeys
2   module ActiveRecord
3     module AssociationPreload
4       def self.append_features(base)
5         super
6         base.send(:extend, ClassMethods)
7       end
8
9       # Composite key versions of Association functions
10       module ClassMethods
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
16
17           if composite?
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 ") + ")"
21             end.join(" OR ")
22
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)
27           else
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
31           end
32
33           conditions.first << append_conditions(reflection, preload_options)
34
35           associated_records = reflection.klass.find(:all,
36             :conditions => conditions,
37             :include    => options[:include],
38             :joins      => joins,
39             :select     => "#{options[:select] || table_name+'.*'}, #{parent_record_id} as parent_record_id_",
40             :order      => options[:order])
41
42           set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'parent_record_id_')
43         end
44
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
49
50           if options[:through]
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
54
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))
62               end
63             end
64           else
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))
67           end
68         end
69
70         def preload_through_records(records, reflection, through_association)
71           through_reflection = reflections[through_association]
72           through_primary_key = through_reflection.primary_key_name
73
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]]}
77
78             records.compact!
79             records.first.class.preload_associations(records, through_association, preload_options)
80
81             # Dont cache the association - we would only be caching a subset
82             through_records = []
83             records.each do |record|
84               proxy = record.send(through_association)
85
86               if proxy.respond_to?(:target)
87                 through_records << proxy.target
88                 proxy.reset
89               else # this is a has_one :through reflection
90                 through_records << proxy if proxy
91               end
92             end
93             through_records.flatten!
94           else
95             records.first.class.preload_associations(records, through_association)
96             through_records = records.map {|record| record.send(through_association)}.flatten
97           end
98
99           through_records.compact!
100           through_records
101         end
102
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)
106
107           if options[:polymorphic]
108             raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
109           else
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:
112             #
113             # { '1,2' => {:id => [1,2], :records => [...records...]}}
114             id_map = {}
115
116             records.each do |record|
117               key = primary_key_name.map{|k| record.send(k)}
118               key_as_string = key.join(CompositePrimaryKeys::ID_SEP)
119
120               if key_as_string
121                 mapped_records = (id_map[key_as_string] ||= {:id => key, :records => []})
122                 mapped_records[:records] << record
123               end
124             end
125
126
127             klasses_and_ids = [[reflection.klass.name, id_map]]
128           end
129
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
135
136             if composite?
137               primary_key = klass.primary_key.to_s.split(CompositePrimaryKeys::ID_SEP)
138               ids = id_map.keys.uniq.map {|id| id_map[id][:id]}
139
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 ") + ")"
142               end.join(" OR ")
143
144               conditions = [where, ids].flatten
145             else
146               conditions = ["#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)", id_map.keys.uniq]
147             end
148
149             conditions.first << append_conditions(reflection, preload_options)
150
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])
157
158             set_association_single_records(id_map, reflection.name, associated_records, primary_key)
159           end
160         end
161
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)
168           end
169         end
170
171         def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
172           seen_keys = {}
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
176
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)
185             end
186           end
187         end
188
189         def find_associated_records(ids, reflection, preload_options)
190           options = reflection.options
191           table_name = reflection.klass.quoted_table_name
192
193           if interface = reflection.options[:as]
194             raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
195           else
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]
199             
200             if composite?
201               foreign_keys = foreign_key.to_s.split(CompositePrimaryKeys::ID_SEP)
202             
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 ") + ")"
205               end.join(" OR ")
206
207               conditions = [where, ids].flatten
208             end
209           end
210
211           conditions.first << append_conditions(reflection, preload_options)
212
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])
220         end        
221         
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 = {}
228           ids = []
229           records.each do |record|
230             primary_key ||= record.class.primary_key
231             ids << record.id
232             mapped_records = (id_to_record_map[record.id.to_s] ||= [])
233             mapped_records << record
234           end
235           ids.uniq!
236           return id_to_record_map, ids
237         end
238         
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)}"
247           end.join(" AND ")
248           "(#{where_clause})"
249         end
250       end
251     end
252   end
253 end