square-activerecord 3.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +6140 -0
- data/README.rdoc +222 -0
- data/examples/associations.png +0 -0
- data/examples/performance.rb +179 -0
- data/examples/simple.rb +14 -0
- data/lib/active_record.rb +124 -0
- data/lib/active_record/aggregations.rb +277 -0
- data/lib/active_record/association_preload.rb +430 -0
- data/lib/active_record/associations.rb +2307 -0
- data/lib/active_record/associations/association_collection.rb +572 -0
- data/lib/active_record/associations/association_proxy.rb +299 -0
- data/lib/active_record/associations/belongs_to_association.rb +91 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +82 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +143 -0
- data/lib/active_record/associations/has_many_association.rb +128 -0
- data/lib/active_record/associations/has_many_through_association.rb +115 -0
- data/lib/active_record/associations/has_one_association.rb +143 -0
- data/lib/active_record/associations/has_one_through_association.rb +40 -0
- data/lib/active_record/associations/through_association_scope.rb +154 -0
- data/lib/active_record/attribute_methods.rb +60 -0
- data/lib/active_record/attribute_methods/before_type_cast.rb +30 -0
- data/lib/active_record/attribute_methods/dirty.rb +95 -0
- data/lib/active_record/attribute_methods/primary_key.rb +56 -0
- data/lib/active_record/attribute_methods/query.rb +39 -0
- data/lib/active_record/attribute_methods/read.rb +145 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +64 -0
- data/lib/active_record/attribute_methods/write.rb +43 -0
- data/lib/active_record/autosave_association.rb +369 -0
- data/lib/active_record/base.rb +1904 -0
- data/lib/active_record/callbacks.rb +284 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +364 -0
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +333 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +73 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +539 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +217 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +657 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +1031 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +401 -0
- data/lib/active_record/counter_cache.rb +115 -0
- data/lib/active_record/dynamic_finder_match.rb +56 -0
- data/lib/active_record/dynamic_scope_match.rb +23 -0
- data/lib/active_record/errors.rb +172 -0
- data/lib/active_record/fixtures.rb +1006 -0
- data/lib/active_record/locale/en.yml +40 -0
- data/lib/active_record/locking/optimistic.rb +172 -0
- data/lib/active_record/locking/pessimistic.rb +55 -0
- data/lib/active_record/log_subscriber.rb +48 -0
- data/lib/active_record/migration.rb +617 -0
- data/lib/active_record/named_scope.rb +138 -0
- data/lib/active_record/nested_attributes.rb +419 -0
- data/lib/active_record/observer.rb +125 -0
- data/lib/active_record/persistence.rb +290 -0
- data/lib/active_record/query_cache.rb +36 -0
- data/lib/active_record/railtie.rb +91 -0
- data/lib/active_record/railties/controller_runtime.rb +38 -0
- data/lib/active_record/railties/databases.rake +512 -0
- data/lib/active_record/reflection.rb +411 -0
- data/lib/active_record/relation.rb +394 -0
- data/lib/active_record/relation/batches.rb +89 -0
- data/lib/active_record/relation/calculations.rb +295 -0
- data/lib/active_record/relation/finder_methods.rb +363 -0
- data/lib/active_record/relation/predicate_builder.rb +48 -0
- data/lib/active_record/relation/query_methods.rb +303 -0
- data/lib/active_record/relation/spawn_methods.rb +132 -0
- data/lib/active_record/schema.rb +59 -0
- data/lib/active_record/schema_dumper.rb +195 -0
- data/lib/active_record/serialization.rb +60 -0
- data/lib/active_record/serializers/xml_serializer.rb +244 -0
- data/lib/active_record/session_store.rb +340 -0
- data/lib/active_record/test_case.rb +67 -0
- data/lib/active_record/timestamp.rb +88 -0
- data/lib/active_record/transactions.rb +359 -0
- data/lib/active_record/validations.rb +84 -0
- data/lib/active_record/validations/associated.rb +48 -0
- data/lib/active_record/validations/uniqueness.rb +190 -0
- data/lib/active_record/version.rb +10 -0
- data/lib/rails/generators/active_record.rb +19 -0
- data/lib/rails/generators/active_record/migration.rb +15 -0
- data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
- data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
- data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
- data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
- data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
- data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
- data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
- data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
- metadata +223 -0
@@ -0,0 +1,572 @@
|
|
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
|