activerecord 3.0.0
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.
- data/CHANGELOG +6023 -0
- data/README.rdoc +222 -0
- data/examples/associations.png +0 -0
- data/examples/performance.rb +162 -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 +403 -0
- data/lib/active_record/associations.rb +2254 -0
- data/lib/active_record/associations/association_collection.rb +562 -0
- data/lib/active_record/associations/association_proxy.rb +295 -0
- data/lib/active_record/associations/belongs_to_association.rb +91 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +78 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +137 -0
- data/lib/active_record/associations/has_many_association.rb +128 -0
- data/lib/active_record/associations/has_many_through_association.rb +116 -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 +33 -0
- data/lib/active_record/attribute_methods/dirty.rb +95 -0
- data/lib/active_record/attribute_methods/primary_key.rb +50 -0
- data/lib/active_record/attribute_methods/query.rb +39 -0
- data/lib/active_record/attribute_methods/read.rb +116 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +61 -0
- data/lib/active_record/attribute_methods/write.rb +37 -0
- data/lib/active_record/autosave_association.rb +369 -0
- data/lib/active_record/base.rb +1867 -0
- data/lib/active_record/callbacks.rb +288 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +365 -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 +329 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +72 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +543 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +212 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +643 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +1030 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -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 +53 -0
- data/lib/active_record/dynamic_scope_match.rb +32 -0
- data/lib/active_record/errors.rb +172 -0
- data/lib/active_record/fixtures.rb +1008 -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 +417 -0
- data/lib/active_record/observer.rb +140 -0
- data/lib/active_record/persistence.rb +291 -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 +403 -0
- data/lib/active_record/relation.rb +393 -0
- data/lib/active_record/relation/batches.rb +89 -0
- data/lib/active_record/relation/calculations.rb +286 -0
- data/lib/active_record/relation/finder_methods.rb +355 -0
- data/lib/active_record/relation/predicate_builder.rb +41 -0
- data/lib/active_record/relation/query_methods.rb +261 -0
- data/lib/active_record/relation/spawn_methods.rb +112 -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 +356 -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 +185 -0
- data/lib/active_record/version.rb +9 -0
- data/lib/rails/generators/active_record.rb +27 -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 +224 -0
@@ -0,0 +1,562 @@
|
|
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[:counter_sql] && !options.blank?
|
189
|
+
raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
|
190
|
+
elsif @reflection.options[:counter_sql]
|
191
|
+
@reflection.klass.count_by_sql(@counter_sql)
|
192
|
+
else
|
193
|
+
|
194
|
+
if @reflection.options[:uniq]
|
195
|
+
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
|
196
|
+
column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" unless column_name
|
197
|
+
options.merge!(:distinct => true)
|
198
|
+
end
|
199
|
+
|
200
|
+
value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
|
201
|
+
|
202
|
+
limit = @reflection.options[:limit]
|
203
|
+
offset = @reflection.options[:offset]
|
204
|
+
|
205
|
+
if limit || offset
|
206
|
+
[ [value - offset.to_i, 0].max, limit.to_i ].min
|
207
|
+
else
|
208
|
+
value
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Removes +records+ from this association calling +before_remove+ and
|
214
|
+
# +after_remove+ callbacks.
|
215
|
+
#
|
216
|
+
# This method is abstract in the sense that +delete_records+ has to be
|
217
|
+
# provided by descendants. Note this method does not imply the records
|
218
|
+
# are actually removed from the database, that depends precisely on
|
219
|
+
# +delete_records+. They are in any case removed from the collection.
|
220
|
+
def delete(*records)
|
221
|
+
remove_records(records) do |_records, old_records|
|
222
|
+
delete_records(old_records) if old_records.any?
|
223
|
+
_records.each { |record| @target.delete(record) }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Destroy +records+ and remove them from this association calling
|
228
|
+
# +before_remove+ and +after_remove+ callbacks.
|
229
|
+
#
|
230
|
+
# Note that this method will _always_ remove records from the database
|
231
|
+
# ignoring the +:dependent+ option.
|
232
|
+
def destroy(*records)
|
233
|
+
records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)}
|
234
|
+
remove_records(records) do |_records, old_records|
|
235
|
+
old_records.each { |record| record.destroy }
|
236
|
+
end
|
237
|
+
|
238
|
+
load_target
|
239
|
+
end
|
240
|
+
|
241
|
+
# Removes all records from this association. Returns +self+ so method calls may be chained.
|
242
|
+
def clear
|
243
|
+
return self if length.zero? # forces load_target if it hasn't happened already
|
244
|
+
|
245
|
+
if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
|
246
|
+
destroy_all
|
247
|
+
else
|
248
|
+
delete_all
|
249
|
+
end
|
250
|
+
|
251
|
+
self
|
252
|
+
end
|
253
|
+
|
254
|
+
# Destroy all the records from this association.
|
255
|
+
#
|
256
|
+
# See destroy for more info.
|
257
|
+
def destroy_all
|
258
|
+
load_target
|
259
|
+
destroy(@target).tap do
|
260
|
+
reset_target!
|
261
|
+
reset_named_scopes_cache!
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def create(attrs = {})
|
266
|
+
if attrs.is_a?(Array)
|
267
|
+
attrs.collect { |attr| create(attr) }
|
268
|
+
else
|
269
|
+
create_record(attrs) do |record|
|
270
|
+
yield(record) if block_given?
|
271
|
+
record.save
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def create!(attrs = {})
|
277
|
+
create_record(attrs) do |record|
|
278
|
+
yield(record) if block_given?
|
279
|
+
record.save!
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Returns the size of the collection by executing a SELECT COUNT(*)
|
284
|
+
# query if the collection hasn't been loaded, and calling
|
285
|
+
# <tt>collection.size</tt> if it has.
|
286
|
+
#
|
287
|
+
# If the collection has been already loaded +size+ and +length+ are
|
288
|
+
# equivalent. If not and you are going to need the records anyway
|
289
|
+
# +length+ will take one less query. Otherwise +size+ is more efficient.
|
290
|
+
#
|
291
|
+
# This method is abstract in the sense that it relies on
|
292
|
+
# +count_records+, which is a method descendants have to provide.
|
293
|
+
def size
|
294
|
+
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
|
295
|
+
@target.size
|
296
|
+
elsif !loaded? && @reflection.options[:group]
|
297
|
+
load_target.size
|
298
|
+
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
|
299
|
+
unsaved_records = @target.select { |r| r.new_record? }
|
300
|
+
unsaved_records.size + count_records
|
301
|
+
else
|
302
|
+
count_records
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# Returns the size of the collection calling +size+ on the target.
|
307
|
+
#
|
308
|
+
# If the collection has been already loaded +length+ and +size+ are
|
309
|
+
# equivalent. If not and you are going to need the records anyway this
|
310
|
+
# method will take one less query. Otherwise +size+ is more efficient.
|
311
|
+
def length
|
312
|
+
load_target.size
|
313
|
+
end
|
314
|
+
|
315
|
+
# Equivalent to <tt>collection.size.zero?</tt>. If the collection has
|
316
|
+
# not been already loaded and you are going to fetch the records anyway
|
317
|
+
# it is better to check <tt>collection.length.zero?</tt>.
|
318
|
+
def empty?
|
319
|
+
size.zero?
|
320
|
+
end
|
321
|
+
|
322
|
+
def any?
|
323
|
+
if block_given?
|
324
|
+
method_missing(:any?) { |*block_args| yield(*block_args) }
|
325
|
+
else
|
326
|
+
!empty?
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# Returns true if the collection has more than 1 record. Equivalent to collection.size > 1.
|
331
|
+
def many?
|
332
|
+
if block_given?
|
333
|
+
method_missing(:many?) { |*block_args| yield(*block_args) }
|
334
|
+
else
|
335
|
+
size > 1
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def uniq(collection = self)
|
340
|
+
seen = Set.new
|
341
|
+
collection.inject([]) do |kept, record|
|
342
|
+
unless seen.include?(record.id)
|
343
|
+
kept << record
|
344
|
+
seen << record.id
|
345
|
+
end
|
346
|
+
kept
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
# Replace this collection with +other_array+
|
351
|
+
# This will perform a diff and delete/add only records that have changed.
|
352
|
+
def replace(other_array)
|
353
|
+
other_array.each { |val| raise_on_type_mismatch(val) }
|
354
|
+
|
355
|
+
load_target
|
356
|
+
other = other_array.size < 100 ? other_array : other_array.to_set
|
357
|
+
current = @target.size < 100 ? @target : @target.to_set
|
358
|
+
|
359
|
+
transaction do
|
360
|
+
delete(@target.select { |v| !other.include?(v) })
|
361
|
+
concat(other_array.select { |v| !current.include?(v) })
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
def include?(record)
|
366
|
+
return false unless record.is_a?(@reflection.klass)
|
367
|
+
load_target if @reflection.options[:finder_sql] && !loaded?
|
368
|
+
return @target.include?(record) if loaded?
|
369
|
+
exists?(record)
|
370
|
+
end
|
371
|
+
|
372
|
+
def proxy_respond_to?(method, include_private = false)
|
373
|
+
super || @reflection.klass.respond_to?(method, include_private)
|
374
|
+
end
|
375
|
+
|
376
|
+
protected
|
377
|
+
def construct_find_options!(options)
|
378
|
+
end
|
379
|
+
|
380
|
+
def construct_counter_sql
|
381
|
+
if @reflection.options[:counter_sql]
|
382
|
+
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
383
|
+
elsif @reflection.options[:finder_sql]
|
384
|
+
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
|
385
|
+
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
|
386
|
+
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
387
|
+
else
|
388
|
+
@counter_sql = @finder_sql
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def load_target
|
393
|
+
if !@owner.new_record? || foreign_key_present
|
394
|
+
begin
|
395
|
+
if !loaded?
|
396
|
+
if @target.is_a?(Array) && @target.any?
|
397
|
+
@target = find_target.map do |f|
|
398
|
+
i = @target.index(f)
|
399
|
+
if i
|
400
|
+
@target.delete_at(i).tap do |t|
|
401
|
+
keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names)
|
402
|
+
t.attributes = f.attributes.except(*keys)
|
403
|
+
end
|
404
|
+
else
|
405
|
+
f
|
406
|
+
end
|
407
|
+
end + @target
|
408
|
+
else
|
409
|
+
@target = find_target
|
410
|
+
end
|
411
|
+
end
|
412
|
+
rescue ActiveRecord::RecordNotFound
|
413
|
+
reset
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
loaded if target
|
418
|
+
target
|
419
|
+
end
|
420
|
+
|
421
|
+
def method_missing(method, *args)
|
422
|
+
match = DynamicFinderMatch.match(method)
|
423
|
+
if match && match.creator?
|
424
|
+
attributes = match.attribute_names
|
425
|
+
return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)])
|
426
|
+
end
|
427
|
+
|
428
|
+
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
|
429
|
+
if block_given?
|
430
|
+
super { |*block_args| yield(*block_args) }
|
431
|
+
else
|
432
|
+
super
|
433
|
+
end
|
434
|
+
elsif @reflection.klass.scopes[method]
|
435
|
+
@_named_scopes_cache ||= {}
|
436
|
+
@_named_scopes_cache[method] ||= {}
|
437
|
+
@_named_scopes_cache[method][args] ||= with_scope(construct_scope) { @reflection.klass.send(method, *args) }
|
438
|
+
else
|
439
|
+
with_scope(construct_scope) do
|
440
|
+
if block_given?
|
441
|
+
@reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }
|
442
|
+
else
|
443
|
+
@reflection.klass.send(method, *args)
|
444
|
+
end
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# overloaded in derived Association classes to provide useful scoping depending on association type.
|
450
|
+
def construct_scope
|
451
|
+
{}
|
452
|
+
end
|
453
|
+
|
454
|
+
def reset_target!
|
455
|
+
@target = Array.new
|
456
|
+
end
|
457
|
+
|
458
|
+
def reset_named_scopes_cache!
|
459
|
+
@_named_scopes_cache = {}
|
460
|
+
end
|
461
|
+
|
462
|
+
def find_target
|
463
|
+
records =
|
464
|
+
if @reflection.options[:finder_sql]
|
465
|
+
@reflection.klass.find_by_sql(@finder_sql)
|
466
|
+
else
|
467
|
+
find(:all)
|
468
|
+
end
|
469
|
+
|
470
|
+
records = @reflection.options[:uniq] ? uniq(records) : records
|
471
|
+
records.each do |record|
|
472
|
+
set_inverse_instance(record, @owner)
|
473
|
+
end
|
474
|
+
records
|
475
|
+
end
|
476
|
+
|
477
|
+
def add_record_to_target_with_callbacks(record)
|
478
|
+
callback(:before_add, record)
|
479
|
+
yield(record) if block_given?
|
480
|
+
@target ||= [] unless loaded?
|
481
|
+
if index = @target.index(record)
|
482
|
+
@target[index] = record
|
483
|
+
else
|
484
|
+
@target << record
|
485
|
+
end
|
486
|
+
callback(:after_add, record)
|
487
|
+
set_inverse_instance(record, @owner)
|
488
|
+
record
|
489
|
+
end
|
490
|
+
|
491
|
+
private
|
492
|
+
def create_record(attrs)
|
493
|
+
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
494
|
+
ensure_owner_is_not_new
|
495
|
+
|
496
|
+
_scope = self.construct_scope[:create]
|
497
|
+
csm = @reflection.klass.send(:current_scoped_methods)
|
498
|
+
options = (csm.blank? || !_scope.is_a?(Hash)) ? _scope : _scope.merge(csm.where_values_hash)
|
499
|
+
|
500
|
+
record = @reflection.klass.send(:with_scope, :create => options) do
|
501
|
+
@reflection.build_association(attrs)
|
502
|
+
end
|
503
|
+
if block_given?
|
504
|
+
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
|
505
|
+
else
|
506
|
+
add_record_to_target_with_callbacks(record)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
def build_record(attrs)
|
511
|
+
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
512
|
+
record = @reflection.build_association(attrs)
|
513
|
+
if block_given?
|
514
|
+
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
|
515
|
+
else
|
516
|
+
add_record_to_target_with_callbacks(record)
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
def remove_records(*records)
|
521
|
+
records = flatten_deeper(records)
|
522
|
+
records.each { |record| raise_on_type_mismatch(record) }
|
523
|
+
|
524
|
+
transaction do
|
525
|
+
records.each { |record| callback(:before_remove, record) }
|
526
|
+
old_records = records.reject { |r| r.new_record? }
|
527
|
+
yield(records, old_records)
|
528
|
+
records.each { |record| callback(:after_remove, record) }
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
def callback(method, record)
|
533
|
+
callbacks_for(method).each do |callback|
|
534
|
+
case callback
|
535
|
+
when Symbol
|
536
|
+
@owner.send(callback, record)
|
537
|
+
when Proc
|
538
|
+
callback.call(@owner, record)
|
539
|
+
else
|
540
|
+
callback.send(method, @owner, record)
|
541
|
+
end
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
def callbacks_for(callback_name)
|
546
|
+
full_callback_name = "#{callback_name}_for_#{@reflection.name}"
|
547
|
+
@owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
|
548
|
+
end
|
549
|
+
|
550
|
+
def ensure_owner_is_not_new
|
551
|
+
if @owner.new_record?
|
552
|
+
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
def fetch_first_or_last_using_find?(args)
|
557
|
+
args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] ||
|
558
|
+
@target.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
end
|