activerecord 3.0.20 → 3.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (122) hide show
  1. data/CHANGELOG +220 -91
  2. data/README.rdoc +3 -3
  3. data/examples/performance.rb +88 -109
  4. data/lib/active_record.rb +6 -2
  5. data/lib/active_record/aggregations.rb +22 -45
  6. data/lib/active_record/associations.rb +264 -991
  7. data/lib/active_record/associations/alias_tracker.rb +85 -0
  8. data/lib/active_record/associations/association.rb +231 -0
  9. data/lib/active_record/associations/association_scope.rb +120 -0
  10. data/lib/active_record/associations/belongs_to_association.rb +40 -60
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +15 -63
  12. data/lib/active_record/associations/builder/association.rb +53 -0
  13. data/lib/active_record/associations/builder/belongs_to.rb +85 -0
  14. data/lib/active_record/associations/builder/collection_association.rb +75 -0
  15. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +63 -0
  16. data/lib/active_record/associations/builder/has_many.rb +65 -0
  17. data/lib/active_record/associations/builder/has_one.rb +63 -0
  18. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  19. data/lib/active_record/associations/collection_association.rb +524 -0
  20. data/lib/active_record/associations/collection_proxy.rb +125 -0
  21. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +27 -118
  22. data/lib/active_record/associations/has_many_association.rb +50 -79
  23. data/lib/active_record/associations/has_many_through_association.rb +98 -67
  24. data/lib/active_record/associations/has_one_association.rb +45 -115
  25. data/lib/active_record/associations/has_one_through_association.rb +21 -25
  26. data/lib/active_record/associations/join_dependency.rb +215 -0
  27. data/lib/active_record/associations/join_dependency/join_association.rb +150 -0
  28. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  29. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  30. data/lib/active_record/associations/join_helper.rb +56 -0
  31. data/lib/active_record/associations/preloader.rb +177 -0
  32. data/lib/active_record/associations/preloader/association.rb +126 -0
  33. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  34. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  35. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  36. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  37. data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
  38. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  39. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  40. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  41. data/lib/active_record/associations/preloader/through_association.rb +67 -0
  42. data/lib/active_record/associations/singular_association.rb +55 -0
  43. data/lib/active_record/associations/through_association.rb +80 -0
  44. data/lib/active_record/attribute_methods.rb +19 -5
  45. data/lib/active_record/attribute_methods/before_type_cast.rb +9 -8
  46. data/lib/active_record/attribute_methods/dirty.rb +8 -2
  47. data/lib/active_record/attribute_methods/primary_key.rb +33 -13
  48. data/lib/active_record/attribute_methods/read.rb +17 -17
  49. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -4
  50. data/lib/active_record/attribute_methods/write.rb +2 -1
  51. data/lib/active_record/autosave_association.rb +66 -45
  52. data/lib/active_record/base.rb +445 -273
  53. data/lib/active_record/callbacks.rb +24 -33
  54. data/lib/active_record/coders/yaml_column.rb +41 -0
  55. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +106 -13
  56. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +16 -2
  57. data/lib/active_record/connection_adapters/abstract/database_limits.rb +12 -11
  58. data/lib/active_record/connection_adapters/abstract/database_statements.rb +83 -12
  59. data/lib/active_record/connection_adapters/abstract/query_cache.rb +16 -16
  60. data/lib/active_record/connection_adapters/abstract/quoting.rb +61 -22
  61. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +16 -273
  62. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +80 -42
  63. data/lib/active_record/connection_adapters/abstract_adapter.rb +44 -25
  64. data/lib/active_record/connection_adapters/column.rb +268 -0
  65. data/lib/active_record/connection_adapters/mysql2_adapter.rb +686 -0
  66. data/lib/active_record/connection_adapters/mysql_adapter.rb +331 -88
  67. data/lib/active_record/connection_adapters/postgresql_adapter.rb +295 -267
  68. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +3 -7
  69. data/lib/active_record/connection_adapters/sqlite_adapter.rb +108 -26
  70. data/lib/active_record/counter_cache.rb +7 -4
  71. data/lib/active_record/fixtures.rb +174 -192
  72. data/lib/active_record/identity_map.rb +131 -0
  73. data/lib/active_record/locking/optimistic.rb +20 -14
  74. data/lib/active_record/locking/pessimistic.rb +4 -4
  75. data/lib/active_record/log_subscriber.rb +24 -4
  76. data/lib/active_record/migration.rb +265 -144
  77. data/lib/active_record/migration/command_recorder.rb +103 -0
  78. data/lib/active_record/named_scope.rb +68 -25
  79. data/lib/active_record/nested_attributes.rb +58 -15
  80. data/lib/active_record/observer.rb +3 -7
  81. data/lib/active_record/persistence.rb +58 -38
  82. data/lib/active_record/query_cache.rb +25 -3
  83. data/lib/active_record/railtie.rb +21 -12
  84. data/lib/active_record/railties/console_sandbox.rb +6 -0
  85. data/lib/active_record/railties/databases.rake +147 -116
  86. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  87. data/lib/active_record/reflection.rb +176 -44
  88. data/lib/active_record/relation.rb +125 -49
  89. data/lib/active_record/relation/batches.rb +7 -5
  90. data/lib/active_record/relation/calculations.rb +50 -18
  91. data/lib/active_record/relation/finder_methods.rb +47 -26
  92. data/lib/active_record/relation/predicate_builder.rb +24 -21
  93. data/lib/active_record/relation/query_methods.rb +117 -101
  94. data/lib/active_record/relation/spawn_methods.rb +27 -20
  95. data/lib/active_record/result.rb +34 -0
  96. data/lib/active_record/schema.rb +5 -6
  97. data/lib/active_record/schema_dumper.rb +11 -13
  98. data/lib/active_record/serialization.rb +2 -2
  99. data/lib/active_record/serializers/xml_serializer.rb +10 -10
  100. data/lib/active_record/session_store.rb +8 -2
  101. data/lib/active_record/test_case.rb +9 -20
  102. data/lib/active_record/timestamp.rb +21 -9
  103. data/lib/active_record/transactions.rb +16 -15
  104. data/lib/active_record/validations.rb +21 -22
  105. data/lib/active_record/validations/associated.rb +3 -1
  106. data/lib/active_record/validations/uniqueness.rb +48 -58
  107. data/lib/active_record/version.rb +3 -3
  108. data/lib/rails/generators/active_record.rb +6 -0
  109. data/lib/rails/generators/active_record/migration/templates/migration.rb +10 -2
  110. data/lib/rails/generators/active_record/model/model_generator.rb +2 -1
  111. data/lib/rails/generators/active_record/model/templates/migration.rb +6 -5
  112. data/lib/rails/generators/active_record/model/templates/model.rb +2 -0
  113. data/lib/rails/generators/active_record/model/templates/module.rb +2 -0
  114. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  115. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +2 -1
  116. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +2 -2
  117. metadata +106 -77
  118. checksums.yaml +0 -7
  119. data/lib/active_record/association_preload.rb +0 -431
  120. data/lib/active_record/associations/association_collection.rb +0 -572
  121. data/lib/active_record/associations/association_proxy.rb +0 -304
  122. data/lib/active_record/associations/through_association_scope.rb +0 -160
checksums.yaml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- SHA1:
3
- metadata.gz: 018450ec0bab2ac9c4463b87ba81b71a1f8e3c50
4
- data.tar.gz: 114e389d25b5a17e40a687fb7dc91a03098ed6d3
5
- SHA512:
6
- metadata.gz: 33533a3b4ff77a3493e70dfec8e87c53ebbbaaca4a7ac38b7d6fa35c2888a49c2071bc96dc0cdba5194d6b3ee771ed4143bc034c85ccd6ad7b6d494e33991092
7
- data.tar.gz: 793f4d22187c8bb59daa512a235f22ececc0caf11171c0012c2d21d9e2de7b9a7ae714ac5715caadc89dab1c63eeea128ca71c336a3931d60d21a87c0040d671
@@ -1,431 +0,0 @@
1
- require 'active_support/core_ext/array/wrap'
2
- require 'active_support/core_ext/enumerable'
3
-
4
- module ActiveRecord
5
- # See ActiveRecord::AssociationPreload::ClassMethods for documentation.
6
- module AssociationPreload #:nodoc:
7
- extend ActiveSupport::Concern
8
-
9
- # Implements the details of eager loading of Active Record associations.
10
- # Application developers should not use this module directly.
11
- #
12
- # <tt>ActiveRecord::Base</tt> is extended with this module. The source code in
13
- # <tt>ActiveRecord::Base</tt> references methods defined in this module.
14
- #
15
- # Note that 'eager loading' and 'preloading' are actually the same thing.
16
- # However, there are two different eager loading strategies.
17
- #
18
- # The first one is by using table joins. This was only strategy available
19
- # prior to Rails 2.1. Suppose that you have an Author model with columns
20
- # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
21
- # this strategy, Active Record would try to retrieve all data for an author
22
- # and all of its books via a single query:
23
- #
24
- # SELECT * FROM authors
25
- # LEFT OUTER JOIN books ON authors.id = books.id
26
- # WHERE authors.name = 'Ken Akamatsu'
27
- #
28
- # However, this could result in many rows that contain redundant data. After
29
- # having received the first row, we already have enough data to instantiate
30
- # the Author object. In all subsequent rows, only the data for the joined
31
- # 'books' table is useful; the joined 'authors' data is just redundant, and
32
- # processing this redundant data takes memory and CPU time. The problem
33
- # quickly becomes worse and worse as the level of eager loading increases
34
- # (i.e. if Active Record is to eager load the associations' associations as
35
- # well).
36
- #
37
- # The second strategy is to use multiple database queries, one for each
38
- # level of association. Since Rails 2.1, this is the default strategy. In
39
- # situations where a table join is necessary (e.g. when the +:conditions+
40
- # option references an association's column), it will fallback to the table
41
- # join strategy.
42
- #
43
- # See also ActiveRecord::Associations::ClassMethods, which explains eager
44
- # loading in a more high-level (application developer-friendly) manner.
45
- module ClassMethods
46
- protected
47
-
48
- # Eager loads the named associations for the given Active Record record(s).
49
- #
50
- # In this description, 'association name' shall refer to the name passed
51
- # to an association creation method. For example, a model that specifies
52
- # <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
53
- # names +:author+ and +:buyers+.
54
- #
55
- # == Parameters
56
- # +records+ is an array of ActiveRecord::Base. This array needs not be flat,
57
- # i.e. +records+ itself may also contain arrays of records. In any case,
58
- # +preload_associations+ will preload the all associations records by
59
- # flattening +records+.
60
- #
61
- # +associations+ specifies one or more associations that you want to
62
- # preload. It may be:
63
- # - a Symbol or a String which specifies a single association name. For
64
- # example, specifying +:books+ allows this method to preload all books
65
- # for an Author.
66
- # - an Array which specifies multiple association names. This array
67
- # is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
68
- # allows this method to preload an author's avatar as well as all of his
69
- # books.
70
- # - a Hash which specifies multiple association names, as well as
71
- # association names for the to-be-preloaded association objects. For
72
- # example, specifying <tt>{ :author => :avatar }</tt> will preload a
73
- # book's author, as well as that author's avatar.
74
- #
75
- # +:associations+ has the same format as the +:include+ option for
76
- # <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
77
- #
78
- # :books
79
- # [ :books, :author ]
80
- # { :author => :avatar }
81
- # [ :books, { :author => :avatar } ]
82
- #
83
- # +preload_options+ contains options that will be passed to ActiveRecord::Base#find
84
- # (which is called under the hood for preloading records). But it is passed
85
- # only one level deep in the +associations+ argument, i.e. it's not passed
86
- # to the child associations when +associations+ is a Hash.
87
- def preload_associations(records, associations, preload_options={})
88
- records = Array.wrap(records).compact
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 = parents.uniq if reflection.macro == :belongs_to
101
- parents.first.class.preload_associations(parents, child)
102
- end
103
- end
104
- end
105
- end
106
-
107
- private
108
-
109
- # Preloads a specific named association for the given records. This is
110
- # called by +preload_associations+ as its base case.
111
- def preload_one_association(records, association, preload_options={})
112
- class_to_reflection = {}
113
- # Not all records have the same class, so group then preload
114
- # group on the reflection itself so that if various subclass share the same association then
115
- # we do not split them unnecessarily
116
- records.group_by { |record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, _records|
117
- raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
118
-
119
- # 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
120
- # the following could call 'preload_belongs_to_association',
121
- # 'preload_has_many_association', etc.
122
- send("preload_#{reflection.macro}_association", _records, reflection, preload_options)
123
- end
124
- end
125
-
126
- def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record)
127
- parent_records.each do |parent_record|
128
- association_proxy = parent_record.send(reflection_name)
129
- association_proxy.loaded
130
- association_proxy.target.push(*Array.wrap(associated_record))
131
-
132
- association_proxy.__send__(:set_inverse_instance, associated_record, parent_record)
133
- end
134
- end
135
-
136
- def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
137
- parent_records.each do |parent_record|
138
- parent_record.send("set_#{reflection_name}_target", associated_record)
139
- end
140
- end
141
-
142
- def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
143
- associated_records.each do |associated_record|
144
- mapped_records = id_to_record_map[associated_record[key].to_s]
145
- add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
146
- end
147
- end
148
-
149
- def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
150
- seen_keys = {}
151
- associated_records.each do |associated_record|
152
- #this is a has_one or belongs_to: there should only be one record.
153
- #Unfortunately we can't (in portable way) ask the database for
154
- #'all records where foo_id in (x,y,z), but please
155
- # only one row per distinct foo_id' so this where we enforce that
156
- next if seen_keys[associated_record[key].to_s]
157
- seen_keys[associated_record[key].to_s] = true
158
- mapped_records = id_to_record_map[associated_record[key].to_s]
159
- mapped_records.each do |mapped_record|
160
- association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record)
161
- association_proxy.__send__(:set_inverse_instance, associated_record, mapped_record)
162
- end
163
- end
164
-
165
- id_to_record_map.each do |id, records|
166
- next if seen_keys.include?(id.to_s)
167
- records.each {|record| record.send("set_#{reflection_name}_target", nil) }
168
- end
169
- end
170
-
171
- # Given a collection of Active Record objects, constructs a Hash which maps
172
- # the objects' IDs to the relevant objects. Returns a 2-tuple
173
- # <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash,
174
- # and +ids+ is an Array of record IDs.
175
- def construct_id_map(records, primary_key=nil)
176
- id_to_record_map = {}
177
- ids = []
178
- records.each do |record|
179
- primary_key ||= record.class.primary_key
180
- ids << record[primary_key]
181
- mapped_records = (id_to_record_map[ids.last.to_s] ||= [])
182
- mapped_records << record
183
- end
184
- ids.uniq!
185
- return id_to_record_map, ids
186
- end
187
-
188
- def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
189
- table_name = reflection.klass.quoted_table_name
190
- id_to_record_map, ids = construct_id_map(records)
191
- records.each {|record| record.send(reflection.name).loaded}
192
- options = reflection.options
193
-
194
- conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}"
195
- conditions << append_conditions(reflection, preload_options)
196
-
197
- associated_records_proxy = reflection.klass.unscoped.
198
- includes(options[:include]).
199
- 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}").
200
- select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id").
201
- order(options[:order])
202
-
203
- all_associated_records = associated_records(ids) do |some_ids|
204
- associated_records_proxy.where([conditions, ids]).to_a
205
- end
206
-
207
- set_association_collection_records(id_to_record_map, reflection.name, all_associated_records, 'the_parent_record_id')
208
- end
209
-
210
- def preload_has_one_association(records, reflection, preload_options={})
211
- return if records.first.send("loaded_#{reflection.name}?")
212
- id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key])
213
- options = reflection.options
214
- records.each {|record| record.send("set_#{reflection.name}_target", nil)}
215
- if options[:through]
216
- through_records = preload_through_records(records, reflection, options[:through])
217
- through_reflection = reflections[options[:through]]
218
- through_primary_key = through_reflection.primary_key_name
219
- unless through_records.empty?
220
- source = reflection.source_reflection.name
221
- through_records.first.class.preload_associations(through_records, source)
222
- if through_reflection.macro == :belongs_to
223
- rev_id_to_record_map, rev_ids = construct_id_map(records, through_primary_key)
224
- rev_primary_key = through_reflection.klass.primary_key
225
- through_records.each do |through_record|
226
- add_preloaded_record_to_collection(rev_id_to_record_map[through_record[rev_primary_key].to_s],
227
- reflection.name, through_record.send(source))
228
- end
229
- else
230
- through_records.each do |through_record|
231
- add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
232
- reflection.name, through_record.send(source))
233
- end
234
- end
235
- end
236
- else
237
- set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
238
- end
239
- end
240
-
241
- def preload_has_many_association(records, reflection, preload_options={})
242
- return if records.first.send(reflection.name).loaded?
243
- options = reflection.options
244
-
245
- primary_key_name = reflection.through_reflection_primary_key_name
246
- id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key])
247
- records.each {|record| record.send(reflection.name).loaded}
248
-
249
- if options[:through]
250
- through_records = preload_through_records(records, reflection, options[:through])
251
- through_reflection = reflections[options[:through]]
252
- unless through_records.empty?
253
- source = reflection.source_reflection.name
254
- through_records.first.class.preload_associations(through_records, source, options)
255
- through_records.each do |through_record|
256
- through_record_id = through_record[reflection.through_reflection_primary_key].to_s
257
- add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source))
258
- end
259
- end
260
-
261
- else
262
- set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
263
- reflection.primary_key_name)
264
- end
265
- end
266
-
267
- def preload_through_records(records, reflection, through_association)
268
- through_reflection = reflections[through_association]
269
- through_primary_key = through_reflection.primary_key_name
270
-
271
- through_records = []
272
- if reflection.options[:source_type]
273
- interface = reflection.source_reflection.options[:foreign_type]
274
- preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
275
-
276
- records.compact!
277
- records.first.class.preload_associations(records, through_association, preload_options)
278
-
279
- # Dont cache the association - we would only be caching a subset
280
- records.each do |record|
281
- proxy = record.send(through_association)
282
-
283
- if proxy.respond_to?(:target)
284
- through_records.concat Array.wrap(proxy.target)
285
- proxy.reset
286
- else # this is a has_one :through reflection
287
- through_records << proxy if proxy
288
- end
289
- end
290
- else
291
- options = {}
292
- options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions] || reflection.options[:order]
293
- options[:order] = reflection.options[:order]
294
- options[:conditions] = reflection.options[:conditions]
295
- records.first.class.preload_associations(records, through_association, options)
296
-
297
- records.each do |record|
298
- through_records.concat Array.wrap(record.send(through_association))
299
- end
300
- end
301
- through_records
302
- end
303
-
304
- def preload_belongs_to_association(records, reflection, preload_options={})
305
- return if records.first.send("loaded_#{reflection.name}?")
306
- options = reflection.options
307
- primary_key_name = reflection.primary_key_name
308
-
309
- if options[:polymorphic]
310
- polymorph_type = options[:foreign_type]
311
- klasses_and_ids = {}
312
-
313
- # Construct a mapping from klass to a list of ids to load and a mapping of those ids back
314
- # to their parent_records
315
- records.each do |record|
316
- if klass = record.send(polymorph_type)
317
- klass_id = record.send(primary_key_name)
318
- if klass_id
319
- id_map = klasses_and_ids[klass] ||= {}
320
- id_list_for_klass_id = (id_map[klass_id.to_s] ||= [])
321
- id_list_for_klass_id << record
322
- end
323
- end
324
- end
325
- klasses_and_ids = klasses_and_ids.to_a
326
- else
327
- id_map = {}
328
- records.each do |record|
329
- key = record.send(primary_key_name)
330
- if key
331
- mapped_records = (id_map[key.to_s] ||= [])
332
- mapped_records << record
333
- end
334
- end
335
- klasses_and_ids = [[reflection.klass.name, id_map]]
336
- end
337
-
338
- klasses_and_ids.each do |klass_and_id|
339
- klass_name, id_map = *klass_and_id
340
- next if id_map.empty?
341
- klass = klass_name.constantize
342
-
343
- table_name = klass.quoted_table_name
344
- primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s
345
- column_type = klass.columns.detect{|c| c.name == primary_key}.type
346
- ids = id_map.keys.map do |id|
347
- if column_type == :integer
348
- id.to_i
349
- elsif column_type == :float
350
- id.to_f
351
- else
352
- id
353
- end
354
- end
355
-
356
- conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
357
- conditions << append_conditions(reflection, preload_options)
358
-
359
- associated_records = klass.unscoped.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a
360
-
361
- set_association_single_records(id_map, reflection.name, associated_records, primary_key)
362
- end
363
- end
364
-
365
- def find_associated_records(ids, reflection, preload_options)
366
- options = reflection.options
367
- table_name = reflection.klass.quoted_table_name
368
-
369
- if interface = reflection.options[:as]
370
- 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}'"
371
- else
372
- foreign_key = reflection.primary_key_name
373
- conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}"
374
- end
375
-
376
- conditions << append_conditions(reflection, preload_options)
377
-
378
- find_options = {
379
- :select => preload_options[:select] || options[:select] || Arel::SqlLiteral.new("#{table_name}.*"),
380
- :include => preload_options[:include] || options[:include],
381
- :joins => options[:joins],
382
- :group => preload_options[:group] || options[:group],
383
- :order => preload_options[:order] || options[:order]
384
- }
385
-
386
- associated_records(ids) do |some_ids|
387
- reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => [conditions, some_ids])).to_a
388
- end
389
- end
390
-
391
- def process_conditions_for_preload(conditions, klass = self)
392
- sanitized = klass.send(:sanitize_sql, conditions)
393
-
394
- if sanitized =~ /\#\{.*\}/
395
- ActiveSupport::Deprecation.warn(
396
- 'String-based interpolation of association conditions is deprecated. Please use a ' \
397
- 'proc instead. So, for example, has_many :older_friends, :conditions => \'age > #{age}\' ' \
398
- 'should be changed to has_many :older_friends, :conditions => proc { "age > #{age}" }.'
399
- )
400
- instance_eval("%@#{sanitized.gsub('@', '\@')}@", __FILE__, __LINE__)
401
- elsif conditions.respond_to?(:to_proc)
402
- klass.send(:sanitize_sql, instance_eval(&conditions))
403
- else
404
- sanitized
405
- end
406
- end
407
-
408
- def append_conditions(reflection, preload_options)
409
- sql = ""
410
- sql << " AND (#{process_conditions_for_preload(reflection.options[:conditions], reflection.klass)})" if reflection.options[:conditions]
411
- sql << " AND (#{process_conditions_for_preload(preload_options[:conditions])})" if preload_options[:conditions]
412
- sql
413
- end
414
-
415
- def in_or_equals_for_ids(ids)
416
- ids.size > 1 ? "IN (?)" : "= ?"
417
- end
418
-
419
- # Some databases impose a limit on the number of ids in a list (in Oracle its 1000)
420
- # Make several smaller queries if necessary or make one query if the adapter supports it
421
- def associated_records(ids)
422
- max_ids_in_a_list = connection.ids_in_list_limit || ids.size
423
- records = []
424
- ids.each_slice(max_ids_in_a_list) do |some_ids|
425
- records += yield(some_ids)
426
- end
427
- records
428
- end
429
- end
430
- end
431
- end
@@ -1,572 +0,0 @@
1
- require 'set'
2
- require 'active_support/core_ext/array/wrap'
3
-
4
- module ActiveRecord
5
- module Associations
6
- # = Active Record Association Collection
7
- #
8
- # AssociationCollection is an abstract class that provides common stuff to
9
- # ease the implementation of association proxies that represent
10
- # collections. See the class hierarchy in AssociationProxy.
11
- #
12
- # You need to be careful with assumptions regarding the target: The proxy
13
- # does not fetch records from the database until it needs them, but new
14
- # ones created with +build+ are added to the target. So, the target may be
15
- # non-empty and still lack children waiting to be read from the database.
16
- # If you look directly to the database you cannot assume that's the entire
17
- # collection because new records may have been added to the target, etc.
18
- #
19
- # If you need to work on all current children, new and existing records,
20
- # +load_target+ and the +loaded+ flag are your friends.
21
- class AssociationCollection < AssociationProxy #:nodoc:
22
- def initialize(owner, reflection)
23
- super
24
- construct_sql
25
- end
26
-
27
- delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped
28
-
29
- def select(select = nil)
30
- if block_given?
31
- load_target
32
- @target.select.each { |e| yield e }
33
- else
34
- scoped.select(select)
35
- end
36
- end
37
-
38
- def scoped
39
- with_scope(construct_scope) { @reflection.klass.scoped }
40
- end
41
-
42
- def find(*args)
43
- options = args.extract_options!
44
-
45
- # If using a custom finder_sql, scan the entire collection.
46
- if @reflection.options[:finder_sql]
47
- expects_array = args.first.kind_of?(Array)
48
- ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
49
-
50
- if ids.size == 1
51
- id = ids.first
52
- record = load_target.detect { |r| id == r.id }
53
- expects_array ? [ record ] : record
54
- else
55
- load_target.select { |r| ids.include?(r.id) }
56
- end
57
- else
58
- merge_options_from_reflection!(options)
59
- construct_find_options!(options)
60
-
61
- find_scope = construct_scope[:find].slice(:conditions, :order)
62
-
63
- with_scope(:find => find_scope) do
64
- relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods))
65
-
66
- case args.first
67
- when :first, :last
68
- relation.send(args.first)
69
- when :all
70
- records = relation.all
71
- @reflection.options[:uniq] ? uniq(records) : records
72
- else
73
- relation.find(*args)
74
- end
75
- end
76
- end
77
- end
78
-
79
- # Fetches the first one using SQL if possible.
80
- def first(*args)
81
- if fetch_first_or_last_using_find?(args)
82
- find(:first, *args)
83
- else
84
- load_target unless loaded?
85
- @target.first(*args)
86
- end
87
- end
88
-
89
- # Fetches the last one using SQL if possible.
90
- def last(*args)
91
- if fetch_first_or_last_using_find?(args)
92
- find(:last, *args)
93
- else
94
- load_target unless loaded?
95
- @target.last(*args)
96
- end
97
- end
98
-
99
- def to_ary
100
- load_target
101
- if @target.is_a?(Array)
102
- @target.to_ary
103
- else
104
- Array.wrap(@target)
105
- end
106
- end
107
- alias_method :to_a, :to_ary
108
-
109
- def reset
110
- reset_target!
111
- reset_named_scopes_cache!
112
- @loaded = false
113
- end
114
-
115
- def build(attributes = {}, &block)
116
- if attributes.is_a?(Array)
117
- attributes.collect { |attr| build(attr, &block) }
118
- else
119
- build_record(attributes) do |record|
120
- block.call(record) if block_given?
121
- set_belongs_to_association_for(record)
122
- end
123
- end
124
- end
125
-
126
- # Add +records+ to this association. Returns +self+ so method calls may be chained.
127
- # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
128
- def <<(*records)
129
- result = true
130
- load_target if @owner.new_record?
131
-
132
- transaction do
133
- flatten_deeper(records).each do |record|
134
- raise_on_type_mismatch(record)
135
- add_record_to_target_with_callbacks(record) do |r|
136
- result &&= insert_record(record) unless @owner.new_record?
137
- end
138
- end
139
- end
140
-
141
- result && self
142
- end
143
-
144
- alias_method :push, :<<
145
- alias_method :concat, :<<
146
-
147
- # Starts a transaction in the association class's database connection.
148
- #
149
- # class Author < ActiveRecord::Base
150
- # has_many :books
151
- # end
152
- #
153
- # Author.first.books.transaction do
154
- # # same effect as calling Book.transaction
155
- # end
156
- def transaction(*args)
157
- @reflection.klass.transaction(*args) do
158
- yield
159
- end
160
- end
161
-
162
- # Remove all records from this association
163
- #
164
- # See delete for more info.
165
- def delete_all
166
- load_target
167
- delete(@target)
168
- reset_target!
169
- reset_named_scopes_cache!
170
- end
171
-
172
- # Calculate sum using SQL, not Enumerable
173
- def sum(*args)
174
- if block_given?
175
- calculate(:sum, *args) { |*block_args| yield(*block_args) }
176
- else
177
- calculate(:sum, *args)
178
- end
179
- end
180
-
181
- # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
182
- # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
183
- # descendant's +construct_sql+ method will have set :counter_sql automatically.
184
- # Otherwise, construct options and pass them with scope to the target class's +count+.
185
- def count(column_name = nil, options = {})
186
- column_name, options = nil, column_name if column_name.is_a?(Hash)
187
-
188
- if @reflection.options[:finder_sql] || @reflection.options[:counter_sql]
189
- unless options.blank?
190
- raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
191
- end
192
-
193
- @reflection.klass.count_by_sql(@counter_sql)
194
- else
195
-
196
- if @reflection.options[:uniq]
197
- # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
198
- column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" unless column_name
199
- options.merge!(:distinct => true)
200
- end
201
-
202
- value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
203
-
204
- limit = @reflection.options[:limit]
205
- offset = @reflection.options[:offset]
206
-
207
- if limit || offset
208
- [ [value - offset.to_i, 0].max, limit.to_i ].min
209
- else
210
- value
211
- end
212
- end
213
- end
214
-
215
- # Removes +records+ from this association calling +before_remove+ and
216
- # +after_remove+ callbacks.
217
- #
218
- # This method is abstract in the sense that +delete_records+ has to be
219
- # provided by descendants. Note this method does not imply the records
220
- # are actually removed from the database, that depends precisely on
221
- # +delete_records+. They are in any case removed from the collection.
222
- def delete(*records)
223
- remove_records(records) do |_records, old_records|
224
- delete_records(old_records) if old_records.any?
225
- _records.each { |record| @target.delete(record) }
226
- end
227
- end
228
-
229
- # Destroy +records+ and remove them from this association calling
230
- # +before_remove+ and +after_remove+ callbacks.
231
- #
232
- # Note that this method will _always_ remove records from the database
233
- # ignoring the +:dependent+ option.
234
- def destroy(*records)
235
- records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)}
236
- remove_records(records) do |_records, old_records|
237
- old_records.each { |record| record.destroy }
238
- end
239
-
240
- load_target
241
- end
242
-
243
- # Removes all records from this association. Returns +self+ so method calls may be chained.
244
- def clear
245
- return self if length.zero? # forces load_target if it hasn't happened already
246
-
247
- if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
248
- destroy_all
249
- else
250
- delete_all
251
- end
252
-
253
- self
254
- end
255
-
256
- # Destroy all the records from this association.
257
- #
258
- # See destroy for more info.
259
- def destroy_all
260
- load_target
261
- destroy(@target).tap do
262
- reset_target!
263
- reset_named_scopes_cache!
264
- end
265
- end
266
-
267
- def create(attrs = {})
268
- if attrs.is_a?(Array)
269
- attrs.collect { |attr| create(attr) }
270
- else
271
- create_record(attrs) do |record|
272
- yield(record) if block_given?
273
- record.save
274
- end
275
- end
276
- end
277
-
278
- def create!(attrs = {})
279
- create_record(attrs) do |record|
280
- yield(record) if block_given?
281
- record.save!
282
- end
283
- end
284
-
285
- # Returns the size of the collection by executing a SELECT COUNT(*)
286
- # query if the collection hasn't been loaded, and calling
287
- # <tt>collection.size</tt> if it has.
288
- #
289
- # If the collection has been already loaded +size+ and +length+ are
290
- # equivalent. If not and you are going to need the records anyway
291
- # +length+ will take one less query. Otherwise +size+ is more efficient.
292
- #
293
- # This method is abstract in the sense that it relies on
294
- # +count_records+, which is a method descendants have to provide.
295
- def size
296
- if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
297
- @target.size
298
- elsif !loaded? && @reflection.options[:group]
299
- load_target.size
300
- elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
301
- unsaved_records = @target.select { |r| r.new_record? }
302
- unsaved_records.size + count_records
303
- else
304
- count_records
305
- end
306
- end
307
-
308
- # Returns the size of the collection calling +size+ on the target.
309
- #
310
- # If the collection has been already loaded +length+ and +size+ are
311
- # equivalent. If not and you are going to need the records anyway this
312
- # method will take one less query. Otherwise +size+ is more efficient.
313
- def length
314
- load_target.size
315
- end
316
-
317
- # Equivalent to <tt>collection.size.zero?</tt>. If the collection has
318
- # not been already loaded and you are going to fetch the records anyway
319
- # it is better to check <tt>collection.length.zero?</tt>.
320
- def empty?
321
- size.zero?
322
- end
323
-
324
- def any?
325
- if block_given?
326
- method_missing(:any?) { |*block_args| yield(*block_args) }
327
- else
328
- !empty?
329
- end
330
- end
331
-
332
- # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1.
333
- def many?
334
- if block_given?
335
- method_missing(:many?) { |*block_args| yield(*block_args) }
336
- else
337
- size > 1
338
- end
339
- end
340
-
341
- def uniq(collection = self)
342
- seen = Set.new
343
- collection.map do |record|
344
- unless seen.include?(record.id)
345
- seen << record.id
346
- record
347
- end
348
- end.compact
349
- end
350
-
351
- # Replace this collection with +other_array+
352
- # This will perform a diff and delete/add only records that have changed.
353
- def replace(other_array)
354
- other_array.each { |val| raise_on_type_mismatch(val) }
355
-
356
- load_target
357
- other = other_array.size < 100 ? other_array : other_array.to_set
358
- current = @target.size < 100 ? @target : @target.to_set
359
-
360
- transaction do
361
- delete(@target.select { |v| !other.include?(v) })
362
- concat(other_array.select { |v| !current.include?(v) })
363
- end
364
- end
365
-
366
- def include?(record)
367
- return false unless record.is_a?(@reflection.klass)
368
- return include_in_memory?(record) if record.new_record?
369
- load_target if @reflection.options[:finder_sql] && !loaded?
370
- return @target.include?(record) if loaded?
371
- exists?(record)
372
- end
373
-
374
- def proxy_respond_to?(method, include_private = false)
375
- super || @reflection.klass.respond_to?(method, include_private)
376
- end
377
-
378
- protected
379
- def construct_find_options!(options)
380
- end
381
-
382
- def construct_counter_sql
383
- if @reflection.options[:counter_sql]
384
- @counter_sql = interpolate_and_sanitize_sql(@reflection.options[:counter_sql])
385
- elsif @reflection.options[:finder_sql]
386
- # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
387
- @counter_sql = interpolate_and_sanitize_sql(@reflection.options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
388
- else
389
- @counter_sql = @finder_sql
390
- end
391
- end
392
-
393
- def load_target
394
- if !@owner.new_record? || foreign_key_present
395
- begin
396
- if !loaded?
397
- if @target.is_a?(Array) && @target.any?
398
- @target = find_target.map do |f|
399
- i = @target.index(f)
400
- if i
401
- @target.delete_at(i).tap do |t|
402
- keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names)
403
- t.attributes = f.attributes.except(*keys)
404
- end
405
- else
406
- f
407
- end
408
- end + @target
409
- else
410
- @target = find_target
411
- end
412
- end
413
- rescue ActiveRecord::RecordNotFound
414
- reset
415
- end
416
- end
417
-
418
- loaded if target
419
- target
420
- end
421
-
422
- def method_missing(method, *args)
423
- match = DynamicFinderMatch.match(method)
424
- if match && match.creator?
425
- attributes = match.attribute_names
426
- return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)])
427
- end
428
-
429
- if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
430
- if block_given?
431
- super { |*block_args| yield(*block_args) }
432
- else
433
- super
434
- end
435
- elsif @reflection.klass.scopes[method]
436
- @_named_scopes_cache ||= {}
437
- @_named_scopes_cache[method] ||= {}
438
- @_named_scopes_cache[method][args] ||= with_scope(construct_scope) { @reflection.klass.send(method, *args) }
439
- else
440
- with_scope(construct_scope) do
441
- if block_given?
442
- @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }
443
- else
444
- @reflection.klass.send(method, *args)
445
- end
446
- end
447
- end
448
- end
449
-
450
- # overloaded in derived Association classes to provide useful scoping depending on association type.
451
- def construct_scope
452
- {}
453
- end
454
-
455
- def reset_target!
456
- @target = Array.new
457
- end
458
-
459
- def reset_named_scopes_cache!
460
- @_named_scopes_cache = {}
461
- end
462
-
463
- def find_target
464
- records =
465
- if @reflection.options[:finder_sql]
466
- @reflection.klass.find_by_sql(@finder_sql)
467
- else
468
- find(:all)
469
- end
470
-
471
- records = @reflection.options[:uniq] ? uniq(records) : records
472
- records.each do |record|
473
- set_inverse_instance(record, @owner)
474
- end
475
- records
476
- end
477
-
478
- def add_record_to_target_with_callbacks(record)
479
- callback(:before_add, record)
480
- yield(record) if block_given?
481
- @target ||= [] unless loaded?
482
- if index = @target.index(record)
483
- @target[index] = record
484
- else
485
- @target << record
486
- end
487
- callback(:after_add, record)
488
- set_inverse_instance(record, @owner)
489
- record
490
- end
491
-
492
- private
493
- def create_record(attrs)
494
- attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
495
- ensure_owner_is_not_new
496
-
497
- scoped_where = scoped.where_values_hash
498
- create_scope = scoped_where ? construct_scope[:create].merge(scoped_where) : construct_scope[:create]
499
- record = @reflection.klass.send(:with_scope, :create => create_scope) do
500
- @reflection.build_association(attrs)
501
- end
502
- if block_given?
503
- add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
504
- else
505
- add_record_to_target_with_callbacks(record)
506
- end
507
- end
508
-
509
- def build_record(attrs)
510
- attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
511
- record = @reflection.build_association(attrs)
512
- if block_given?
513
- add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
514
- else
515
- add_record_to_target_with_callbacks(record)
516
- end
517
- end
518
-
519
- def remove_records(*records)
520
- records = flatten_deeper(records)
521
- records.each { |record| raise_on_type_mismatch(record) }
522
-
523
- transaction do
524
- records.each { |record| callback(:before_remove, record) }
525
- old_records = records.reject { |r| r.new_record? }
526
- yield(records, old_records)
527
- records.each { |record| callback(:after_remove, record) }
528
- end
529
- end
530
-
531
- def callback(method, record)
532
- callbacks_for(method).each do |callback|
533
- case callback
534
- when Symbol
535
- @owner.send(callback, record)
536
- when Proc
537
- callback.call(@owner, record)
538
- else
539
- callback.send(method, @owner, record)
540
- end
541
- end
542
- end
543
-
544
- def callbacks_for(callback_name)
545
- full_callback_name = "#{callback_name}_for_#{@reflection.name}"
546
- @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
547
- end
548
-
549
- def ensure_owner_is_not_new
550
- if @owner.new_record?
551
- raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
552
- end
553
- end
554
-
555
- def fetch_first_or_last_using_find?(args)
556
- args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] ||
557
- @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
558
- end
559
-
560
- def include_in_memory?(record)
561
- if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
562
- @owner.send(proxy_reflection.through_reflection.name).any? { |source|
563
- target = source.send(proxy_reflection.source_reflection.name)
564
- target.respond_to?(:include?) ? target.include?(record) : target == record
565
- } || @target.include?(record)
566
- else
567
- @target.include?(record)
568
- end
569
- end
570
- end
571
- end
572
- end