forked from mongodb/mongoid
/
many.rb
522 lines (489 loc) · 18.1 KB
/
many.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# encoding: utf-8
module Mongoid # :nodoc:
module Relations #:nodoc:
module Embedded #:nodoc:
# This class handles the behaviour for a document that embeds many other
# documents within in it as an array.
class Many < Relations::Many
include Atomic
# Appends a document or array of documents to the relation. Will set
# the parent and update the index in the process.
#
# @example Append a document.
# person.addresses << address
#
# @example Push a document.
# person.addresses.push(address)
#
# @example Concat with other documents.
# person.addresses.concat([ address_one, address_two ])
#
# @param [ Document, Array<Document> ] *args Any number of documents.
def <<(*args)
options = default_options(args)
atomically(:$pushAll) do
args.flatten.each do |doc|
return doc unless doc
append(doc, options)
doc.save if base.persisted? && !options[:binding]
end
end
end
# Binds the base object to the inverse of the relation. This is so we
# are referenced to the actual objects themselves and dont hit the
# database twice when setting the relations up.
#
# This is called after first creating the relation, or if a new object
# is set on the relation.
#
# @example Bind the relation.
# person.addresses.bind(:continue => true)
#
# @param [ Hash ] options The options to bind with.
#
# @option options [ true, false ] :binding Are we in build mode?
# @option options [ true, false ] :continue Continue binding the
# inverse?
#
# @since 2.0.0.rc.1
def bind(options = {})
binding.bind(options)
if base.persisted? && !options[:binding]
atomically(:$set) { target.each { |doc| doc.save } }
end
end
# Bind the inverse relation between a single document in this proxy
# instead of the entire target.
#
# Used when appending to the target instead of setting the entire
# thing.
#
# @example Bind a single document.
# person.addressses.bind_one(address)
#
# @param [ Document ] document The document to bind.
#
# @since 2.0.0.rc.1
def bind_one(document, options = {})
binding.bind_one(document, options)
end
# Is the relation empty?
#
# @example Is the relation empty??
# person.addresses.blank?
#
# @return [ true, false ] If the relation is empty or not.
#
# @since 2.1.0
def blank?
size == 0
end
# Clear the relation. Will delete the documents from the db if they are
# already persisted.
#
# @example Clear the relation.
# person.addresses.clear
#
# @return [ Many ] The empty relation.
def clear
load! and substitute(nil)
end
# Returns a count of the number of documents in the association that have
# actually been persisted to the database.
#
# Use #size if you want the total number of documents.
#
# @example Get the count of persisted documents.
# person.addresses.count
#
# @return [ Integer ] The total number of persisted embedded docs, as
# flagged by the #persisted? method.
def count
target.select { |doc| doc.persisted? }.size
end
# Create a new document in the relation. This is essentially the same
# as doing a #build then #save on the new document.
#
# @example Create a new document in the relation.
# person.movies.create(:name => "Bozo")
#
# @param [ Hash ] attributes The attributes to build the document with.
# @param [ Class ] type Optional class to create the document with.
#
# @return [ Document ] The newly created document.
def create(attributes = {}, type = nil, &block)
build(attributes, type, &block).tap { |doc| doc.save }
end
# Create a new document in the relation. This is essentially the same
# as doing a #build then #save on the new document. If validation
# failed on the document an error will get raised.
#
# @example Create the document.
# person.addresses.create!(:street => "Unter der Linden")</tt>
#
# @param [ Hash ] attributes The attributes to build the document with.
# @param [ Class ] type Optional class to create the document with.
#
# @raise [ Errors::Validations ] If a validation error occured.
#
# @return [ Document ] The newly created document.
def create!(attributes = {}, type = nil, &block)
build(attributes, type, &block).tap { |doc| doc.save! }
end
# Delete the supplied document from the target. This method is proxied
# in order to reindex the array after the operation occurs.
#
# @example Delete the document from the relation.
# person.addresses.delete(address)
#
# @param [ Document ] document The document to be deleted.
#
# @return [ Document, nil ] The deleted document or nil if nothing deleted.
#
# @since 2.0.0.rc.1
def delete(document)
target.delete(document).tap { reindex }
end
# Delete all the documents in the association without running callbacks.
#
# @example Delete all documents from the relation.
# person.addresses.delete_all
#
# @example Conditionally delete documents from the relation.
# person.addresses.delete_all(:conditions => { :street => "Bond" })
#
# @param [ Hash ] conditions Conditions on which documents to delete.
#
# @return [ Integer ] The number of documents deleted.
def delete_all(conditions = {})
atomically(:$pull) { remove_all(conditions, :delete) }
end
# Destroy all the documents in the association whilst running callbacks.
#
# @example Destroy all documents from the relation.
# person.addresses.destroy_all
#
# @example Conditionally destroy documents from the relation.
# person.addresses.destroy_all(:conditions => { :street => "Bond" })
#
# @param [ Hash ] conditions Conditions on which documents to destroy.
#
# @return [ Integer ] The number of documents destroyed.
def destroy_all(conditions = {})
atomically(:$pull) { remove_all(conditions, :destroy) }
end
# Finds a document in this association through several different
# methods.
#
# @example Find a document by its id.
# person.addresses.find(BSON::ObjectId.new)
#
# @example Find documents for multiple ids.
# person.addresses.find([ BSON::ObjectId.new, BSON::ObjectId.new ])
#
# @example Find documents based on conditions.
# person.addresses.find(:all, :conditions => { :number => 10 })
# person.addresses.find(:first, :conditions => { :number => 10 })
# person.addresses.find(:last, :conditions => { :number => 10 })
#
# @param [ Array<Object> ] args Various arguments.
#
# @return [ Array<Document>, Document ] A single or multiple documents.
def find(*args)
criteria.find(*args)
end
# Instantiate a new embeds_many relation.
#
# @example Create the new relation.
# Many.new(person, addresses, metadata)
#
# @param [ Document ] base The document this relation hangs off of.
# @param [ Array<Document> ] target The child documents of the relation.
# @param [ Metadata ] metadata The relation's metadata
#
# @return [ Many ] The proxy.
def initialize(base, target, metadata)
init(base, target, metadata) do
target.each_with_index do |doc, index|
characterize_one(doc)
doc.parentize(base)
doc._index = index
end
end
end
# Will load the target into an array if the target had not already been
# loaded.
#
# @example Load the relation into memory.
# relation.load!
#
# @return [ Many ] The relation.
#
# @since 2.0.0.rc.5
def load!(options = {})
tap do |relation|
unless relation.loaded?
relation.bind(options)
relation.loaded = true
end
end
end
# Substitutes the supplied target documents for the existing documents
# in the relation.
#
# @example Substitute the relation's target.
# person.addresses.substitute([ address ])
#
# @param [ Array<Document> ] new_target The replacement array.
# @param [ true, false ] building Are we in build mode?
#
# @return [ Many ] The proxied relation.
#
# @since 2.0.0.rc.1
def substitute(new_target, options = {})
old_target = target
tap do |relation|
relation.target = new_target || []
if !new_target.blank?
atomically(:$set) { rebind(old_target, options) }
else
atomically(:$unset) { unbind(old_target, options) }
end
end
end
# Get this relation as as its representation in the database.
#
# @example Convert the relation to an attributes hash.
# person.addresses.as_document
#
# @return [ Array<Hash> ] The relation as stored in the db.
#
# @since 2.0.0.rc.1
def as_document
[].tap do |attributes|
target.each do |doc|
attributes << doc.as_document
end
end
end
# Unbind the inverse relation from this set of documents. Used when the
# entire proxy has been cleared, set to nil or empty, or replaced.
#
# @example Unbind the relation.
# person.addresses.unbind(target, :continue => false)
#
# @param [ Array<Document> ] old_target The relations previous target.
# @param [ Hash ] options The options to bind with.
#
# @option options [ true, false ] :binding Are we in build mode?
# @option options [ true, false ] :continue Continue binding the
# inverse?
#
# @since 2.0.0.rc.1
def unbind(old_target, options = {})
binding(old_target).unbind(options)
if base.persisted?
old_target.each do |doc|
doc.delete unless doc.destroyed?
end
end
end
private
# Appends the document to the target array, updating the index on the
# document at the same time.
#
# @example Append to the document.
# relation.append(document)
#
# @param [ Document ] document The document to append to the target.
#
# @since 2.0.0.rc.1
def append(document, options = {})
load! and target.push(document)
characterize_one(document)
bind_one(document, options)
document._index = target.size - 1
end
# Instantiate the binding associated with this relation.
#
# @example Create the binding.
# relation.binding([ address ])
#
# @param [ Array<Document> ] new_target The new documents to bind with.
#
# @return [ Binding ] The many binding.
#
# @since 2.0.0.rc.1
def binding(new_target = nil)
Bindings::Embedded::Many.new(base, new_target || target, metadata)
end
# Returns the criteria object for the target class with its documents set
# to target.
#
# @example Get a criteria for the relation.
# relation.criteria
#
# @return [ Criteria ] A new criteria.
def criteria
klass.criteria(true).tap do |criterion|
criterion.documents = target
end
end
# If the target array does not respond to the supplied method then try to
# find a named scope or criteria on the class and send the call there.
#
# If the method exists on the array, use the default proxy behavior.
#
# @param [ Symbol, String ] name The name of the method.
# @param [ Array ] args The method args
# @param [ Proc ] block Optional block to pass.
#
# @return [ Criteria, Object ] A Criteria or return value from the target.
def method_missing(name, *args, &block)
load!(:binding => true) and return super if target.respond_to?(name)
klass.send(:with_scope, criteria) do
criteria.send(name, *args, &block)
end
end
# Reindex all the target elements. This is useful when performing
# operations on the proxied target directly and the indices need to
# match that on the database side.
#
# @example Reindex the relation.
# person.addresses.reindex
#
# @since 2.0.0.rc.1
def reindex
target.each_with_index do |doc, index|
doc._index = index
end
end
# Remove all documents from the relation, either with a delete or a
# destroy depending on what this was called through.
#
# @example Destroy documents from the relation.
# relation.remove_all(:conditions => { :num => 1 }, true)
#
# @param [ Hash ] conditions Conditions to filter by.
# @param [ true, false ] destroy If true then destroy, else delete.
#
# @return [ Integer ] The number of documents removed.
def remove_all(conditions = {}, method = :delete)
criteria = find(:all, conditions || {})
criteria.size.tap do
criteria.each do |doc|
target.delete_at(target.index(doc))
doc.send(method, :suppress => true)
end
reindex
end
end
# Convenience method to clean up the substitute code. Unbinds the old
# target and reindexes.
#
# @example Rebind the relation.
# relation.rebind([])
#
# @param [ Array<Document> ] old_target The old target.
# @param [ Hash ] options The options passed to substitute.
#
# @since 2.0.0
def rebind(old_target, options)
reindex
unbind(old_target, options)
bind(options)
end
class << self
# Return the builder that is responsible for generating the documents
# that will be used by this relation.
#
# @example Get the builder.
# Embedded::Many.builder(meta, object)
#
# @param [ Metadata ] meta The metadata of the relation.
# @param [ Document, Hash ] object A document or attributes to build
# with.
#
# @return [ Builder ] A newly instantiated builder object.
#
# @since 2.0.0.rc.1
def builder(meta, object, loading = false)
Builders::Embedded::Many.new(meta, object, loading)
end
# Returns true if the relation is an embedded one. In this case
# always true.
#
# @example Is the relation embedded?
# Embedded::Many.embedded?
#
# @return [ true ] true.
#
# @since 2.0.0.rc.1
def embedded?
true
end
# Returns the macro for this relation. Used mostly as a helper in
# reflection.
#
# @example Get the relation macro.
# Mongoid::Relations::Embedded::Many.macro
#
# @return [ Symbol ] :embeds_many
#
# @since 2.0.0.rc.1
def macro
:embeds_many
end
# Return the nested builder that is responsible for generating the
# documents that will be used by this relation.
#
# @example Get the nested builder.
# NestedAttributes::Many.builder(attributes, options)
#
# @param [ Metadata ] metadata The relation metadata.
# @param [ Hash ] attributes The attributes to build with.
# @param [ Hash ] options The builder options.
#
# @option options [ true, false ] :allow_destroy Can documents be
# deleted?
# @option options [ Integer ] :limit Max number of documents to
# create at once.
# @option options [ Proc, Symbol ] :reject_if If documents match this
# option then they are ignored.
# @option options [ true, false ] :update_only Only existing documents
# can be modified.
#
# @return [ NestedBuilder ] The nested attributes builder.
#
# @since 2.0.0.rc.1
def nested_builder(metadata, attributes, options)
Builders::NestedAttributes::Many.new(metadata, attributes, options)
end
# Tells the caller if this relation is one that stores the foreign
# key on its own objects.
#
# @example Does this relation store a foreign key?
# Embedded::Many.stores_foreign_key?
#
# @return [ false ] false.
#
# @since 2.0.0.rc.1
def stores_foreign_key?
false
end
# Get the valid options allowed with this relation.
#
# @example Get the valid options.
# Relation.valid_options
#
# @return [ Array<Symbol> ] The valid options.
#
# @since 2.1.0
def valid_options
[ :as, :cyclic, :order, :versioned ]
end
end
end
end
end
end