duck_record 0.0.20 → 0.0.21

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1160 @@
1
+ module DuckRecord
2
+ module Associations
3
+ # Association proxies in Active Record are middlemen between the object that
4
+ # holds the association, known as the <tt>@owner</tt>, and the actual associated
5
+ # object, known as the <tt>@target</tt>. The kind of association any proxy is
6
+ # about is available in <tt>@reflection</tt>. That's an instance of the class
7
+ # ActiveRecord::Reflection::AssociationReflection.
8
+ #
9
+ # For example, given
10
+ #
11
+ # class Blog < ActiveRecord::Base
12
+ # has_many :posts
13
+ # end
14
+ #
15
+ # blog = Blog.first
16
+ #
17
+ # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
18
+ # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
19
+ # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
20
+ #
21
+ # This class delegates unknown methods to <tt>@target</tt> via
22
+ # <tt>method_missing</tt>.
23
+ #
24
+ # The <tt>@target</tt> object is not \loaded until needed. For example,
25
+ #
26
+ # blog.posts.count
27
+ #
28
+ # is computed directly through SQL and does not trigger by itself the
29
+ # instantiation of the actual post records.
30
+ class CollectionProxy < ActiveRecord::Relation
31
+ def initialize(klass, association) #:nodoc:
32
+ @association = association
33
+ super klass, klass.arel_table, klass.predicate_builder
34
+
35
+ extensions = association.extensions
36
+ extend(*extensions) if extensions.any?
37
+ end
38
+
39
+ def target
40
+ @association.target
41
+ end
42
+
43
+ def load_target
44
+ @association.load_target
45
+ end
46
+
47
+ # Returns +true+ if the association has been loaded, otherwise +false+.
48
+ #
49
+ # person.pets.loaded? # => false
50
+ # person.pets
51
+ # person.pets.loaded? # => true
52
+ def loaded?
53
+ @association.loaded?
54
+ end
55
+
56
+ ##
57
+ # :method: select
58
+ #
59
+ # :call-seq:
60
+ # select(*fields, &block)
61
+ #
62
+ # Works in two ways.
63
+ #
64
+ # *First:* Specify a subset of fields to be selected from the result set.
65
+ #
66
+ # class Person < ActiveRecord::Base
67
+ # has_many :pets
68
+ # end
69
+ #
70
+ # person.pets
71
+ # # => [
72
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
73
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
74
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
75
+ # # ]
76
+ #
77
+ # person.pets.select(:name)
78
+ # # => [
79
+ # # #<Pet id: nil, name: "Fancy-Fancy">,
80
+ # # #<Pet id: nil, name: "Spook">,
81
+ # # #<Pet id: nil, name: "Choo-Choo">
82
+ # # ]
83
+ #
84
+ # person.pets.select(:id, :name)
85
+ # # => [
86
+ # # #<Pet id: 1, name: "Fancy-Fancy">,
87
+ # # #<Pet id: 2, name: "Spook">,
88
+ # # #<Pet id: 3, name: "Choo-Choo">
89
+ # # ]
90
+ #
91
+ # Be careful because this also means you're initializing a model
92
+ # object with only the fields that you've selected. If you attempt
93
+ # to access a field except +id+ that is not in the initialized record you'll
94
+ # receive:
95
+ #
96
+ # person.pets.select(:name).first.person_id
97
+ # # => ActiveModel::MissingAttributeError: missing attribute: person_id
98
+ #
99
+ # *Second:* You can pass a block so it can be used just like Array#select.
100
+ # This builds an array of objects from the database for the scope,
101
+ # converting them into an array and iterating through them using
102
+ # Array#select.
103
+ #
104
+ # person.pets.select { |pet| pet.name =~ /oo/ }
105
+ # # => [
106
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
107
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
108
+ # # ]
109
+
110
+ # Finds an object in the collection responding to the +id+. Uses the same
111
+ # rules as ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound
112
+ # error if the object cannot be found.
113
+ #
114
+ # class Person < ActiveRecord::Base
115
+ # has_many :pets
116
+ # end
117
+ #
118
+ # person.pets
119
+ # # => [
120
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
121
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
122
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
123
+ # # ]
124
+ #
125
+ # person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
126
+ # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=4
127
+ #
128
+ # person.pets.find(2) { |pet| pet.name.downcase! }
129
+ # # => #<Pet id: 2, name: "fancy-fancy", person_id: 1>
130
+ #
131
+ # person.pets.find(2, 3)
132
+ # # => [
133
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
134
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
135
+ # # ]
136
+ def find(*args, &block)
137
+ @association.find(*args, &block)
138
+ end
139
+
140
+ ##
141
+ # :method: first
142
+ #
143
+ # :call-seq:
144
+ # first(limit = nil)
145
+ #
146
+ # Returns the first record, or the first +n+ records, from the collection.
147
+ # If the collection is empty, the first form returns +nil+, and the second
148
+ # form returns an empty array.
149
+ #
150
+ # class Person < ActiveRecord::Base
151
+ # has_many :pets
152
+ # end
153
+ #
154
+ # person.pets
155
+ # # => [
156
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
157
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
158
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
159
+ # # ]
160
+ #
161
+ # person.pets.first # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
162
+ #
163
+ # person.pets.first(2)
164
+ # # => [
165
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
166
+ # # #<Pet id: 2, name: "Spook", person_id: 1>
167
+ # # ]
168
+ #
169
+ # another_person_without.pets # => []
170
+ # another_person_without.pets.first # => nil
171
+ # another_person_without.pets.first(3) # => []
172
+
173
+ ##
174
+ # :method: second
175
+ #
176
+ # :call-seq:
177
+ # second()
178
+ #
179
+ # Same as #first except returns only the second record.
180
+
181
+ ##
182
+ # :method: third
183
+ #
184
+ # :call-seq:
185
+ # third()
186
+ #
187
+ # Same as #first except returns only the third record.
188
+
189
+ ##
190
+ # :method: fourth
191
+ #
192
+ # :call-seq:
193
+ # fourth()
194
+ #
195
+ # Same as #first except returns only the fourth record.
196
+
197
+ ##
198
+ # :method: fifth
199
+ #
200
+ # :call-seq:
201
+ # fifth()
202
+ #
203
+ # Same as #first except returns only the fifth record.
204
+
205
+ ##
206
+ # :method: forty_two
207
+ #
208
+ # :call-seq:
209
+ # forty_two()
210
+ #
211
+ # Same as #first except returns only the forty second record.
212
+ # Also known as accessing "the reddit".
213
+
214
+ ##
215
+ # :method: third_to_last
216
+ #
217
+ # :call-seq:
218
+ # third_to_last()
219
+ #
220
+ # Same as #first except returns only the third-to-last record.
221
+
222
+ ##
223
+ # :method: second_to_last
224
+ #
225
+ # :call-seq:
226
+ # second_to_last()
227
+ #
228
+ # Same as #first except returns only the second-to-last record.
229
+
230
+ # Returns the last record, or the last +n+ records, from the collection.
231
+ # If the collection is empty, the first form returns +nil+, and the second
232
+ # form returns an empty array.
233
+ #
234
+ # class Person < ActiveRecord::Base
235
+ # has_many :pets
236
+ # end
237
+ #
238
+ # person.pets
239
+ # # => [
240
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
241
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
242
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
243
+ # # ]
244
+ #
245
+ # person.pets.last # => #<Pet id: 3, name: "Choo-Choo", person_id: 1>
246
+ #
247
+ # person.pets.last(2)
248
+ # # => [
249
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
250
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
251
+ # # ]
252
+ #
253
+ # another_person_without.pets # => []
254
+ # another_person_without.pets.last # => nil
255
+ # another_person_without.pets.last(3) # => []
256
+ def last(limit = nil)
257
+ load_target if find_from_target?
258
+ super
259
+ end
260
+
261
+ # Gives a record (or N records if a parameter is supplied) from the collection
262
+ # using the same rules as <tt>ActiveRecord::Base.take</tt>.
263
+ #
264
+ # class Person < ActiveRecord::Base
265
+ # has_many :pets
266
+ # end
267
+ #
268
+ # person.pets
269
+ # # => [
270
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
271
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
272
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
273
+ # # ]
274
+ #
275
+ # person.pets.take # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
276
+ #
277
+ # person.pets.take(2)
278
+ # # => [
279
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
280
+ # # #<Pet id: 2, name: "Spook", person_id: 1>
281
+ # # ]
282
+ #
283
+ # another_person_without.pets # => []
284
+ # another_person_without.pets.take # => nil
285
+ # another_person_without.pets.take(2) # => []
286
+ def take(limit = nil)
287
+ load_target if find_from_target?
288
+ super
289
+ end
290
+
291
+ # Returns a new object of the collection type that has been instantiated
292
+ # with +attributes+ and linked to this object, but have not yet been saved.
293
+ # You can pass an array of attributes hashes, this will return an array
294
+ # with the new objects.
295
+ #
296
+ # class Person
297
+ # has_many :pets
298
+ # end
299
+ #
300
+ # person.pets.build
301
+ # # => #<Pet id: nil, name: nil, person_id: 1>
302
+ #
303
+ # person.pets.build(name: 'Fancy-Fancy')
304
+ # # => #<Pet id: nil, name: "Fancy-Fancy", person_id: 1>
305
+ #
306
+ # person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}])
307
+ # # => [
308
+ # # #<Pet id: nil, name: "Spook", person_id: 1>,
309
+ # # #<Pet id: nil, name: "Choo-Choo", person_id: 1>,
310
+ # # #<Pet id: nil, name: "Brain", person_id: 1>
311
+ # # ]
312
+ #
313
+ # person.pets.size # => 5 # size of the collection
314
+ # person.pets.count # => 0 # count from database
315
+ def build(attributes = {}, &block)
316
+ @association.build(attributes, &block)
317
+ end
318
+ alias_method :new, :build
319
+
320
+ # Returns a new object of the collection type that has been instantiated with
321
+ # attributes, linked to this object and that has already been saved (if it
322
+ # passes the validations).
323
+ #
324
+ # class Person
325
+ # has_many :pets
326
+ # end
327
+ #
328
+ # person.pets.create(name: 'Fancy-Fancy')
329
+ # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
330
+ #
331
+ # person.pets.create([{name: 'Spook'}, {name: 'Choo-Choo'}])
332
+ # # => [
333
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
334
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
335
+ # # ]
336
+ #
337
+ # person.pets.size # => 3
338
+ # person.pets.count # => 3
339
+ #
340
+ # person.pets.find(1, 2, 3)
341
+ # # => [
342
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
343
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
344
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
345
+ # # ]
346
+ def create(attributes = {}, &block)
347
+ @association.create(attributes, &block)
348
+ end
349
+
350
+ # Like #create, except that if the record is invalid, raises an exception.
351
+ #
352
+ # class Person
353
+ # has_many :pets
354
+ # end
355
+ #
356
+ # class Pet
357
+ # validates :name, presence: true
358
+ # end
359
+ #
360
+ # person.pets.create!(name: nil)
361
+ # # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
362
+ def create!(attributes = {}, &block)
363
+ @association.create!(attributes, &block)
364
+ end
365
+
366
+ # Add one or more records to the collection by setting their foreign keys
367
+ # to the association's primary key. Since #<< flattens its argument list and
368
+ # inserts each record, +push+ and #concat behave identically. Returns +self+
369
+ # so method calls may be chained.
370
+ #
371
+ # class Person < ActiveRecord::Base
372
+ # has_many :pets
373
+ # end
374
+ #
375
+ # person.pets.size # => 0
376
+ # person.pets.concat(Pet.new(name: 'Fancy-Fancy'))
377
+ # person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo'))
378
+ # person.pets.size # => 3
379
+ #
380
+ # person.id # => 1
381
+ # person.pets
382
+ # # => [
383
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
384
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
385
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
386
+ # # ]
387
+ #
388
+ # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')])
389
+ # person.pets.size # => 5
390
+ def concat(*records)
391
+ @association.concat(*records)
392
+ end
393
+
394
+ # Replaces this collection with +other_array+. This will perform a diff
395
+ # and delete/add only records that have changed.
396
+ #
397
+ # class Person < ActiveRecord::Base
398
+ # has_many :pets
399
+ # end
400
+ #
401
+ # person.pets
402
+ # # => [#<Pet id: 1, name: "Gorby", group: "cats", person_id: 1>]
403
+ #
404
+ # other_pets = [Pet.new(name: 'Puff', group: 'celebrities']
405
+ #
406
+ # person.pets.replace(other_pets)
407
+ #
408
+ # person.pets
409
+ # # => [#<Pet id: 2, name: "Puff", group: "celebrities", person_id: 1>]
410
+ #
411
+ # If the supplied array has an incorrect association type, it raises
412
+ # an <tt>ActiveRecord::AssociationTypeMismatch</tt> error:
413
+ #
414
+ # person.pets.replace(["doo", "ggie", "gaga"])
415
+ # # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String
416
+ def replace(other_array)
417
+ @association.replace(other_array)
418
+ end
419
+
420
+ # Deletes all the records from the collection according to the strategy
421
+ # specified by the +:dependent+ option. If no +:dependent+ option is given,
422
+ # then it will follow the default strategy.
423
+ #
424
+ # For <tt>has_many :through</tt> associations, the default deletion strategy is
425
+ # +:delete_all+.
426
+ #
427
+ # For +has_many+ associations, the default deletion strategy is +:nullify+.
428
+ # This sets the foreign keys to +NULL+.
429
+ #
430
+ # class Person < ActiveRecord::Base
431
+ # has_many :pets # dependent: :nullify option by default
432
+ # end
433
+ #
434
+ # person.pets.size # => 3
435
+ # person.pets
436
+ # # => [
437
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
438
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
439
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
440
+ # # ]
441
+ #
442
+ # person.pets.delete_all
443
+ # # => [
444
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
445
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
446
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
447
+ # # ]
448
+ #
449
+ # person.pets.size # => 0
450
+ # person.pets # => []
451
+ #
452
+ # Pet.find(1, 2, 3)
453
+ # # => [
454
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>,
455
+ # # #<Pet id: 2, name: "Spook", person_id: nil>,
456
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: nil>
457
+ # # ]
458
+ #
459
+ # Both +has_many+ and <tt>has_many :through</tt> dependencies default to the
460
+ # +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+.
461
+ # Records are not instantiated and callbacks will not be fired.
462
+ #
463
+ # class Person < ActiveRecord::Base
464
+ # has_many :pets, dependent: :destroy
465
+ # end
466
+ #
467
+ # person.pets.size # => 3
468
+ # person.pets
469
+ # # => [
470
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
471
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
472
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
473
+ # # ]
474
+ #
475
+ # person.pets.delete_all
476
+ #
477
+ # Pet.find(1, 2, 3)
478
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
479
+ #
480
+ # If it is set to <tt>:delete_all</tt>, all the objects are deleted
481
+ # *without* calling their +destroy+ method.
482
+ #
483
+ # class Person < ActiveRecord::Base
484
+ # has_many :pets, dependent: :delete_all
485
+ # end
486
+ #
487
+ # person.pets.size # => 3
488
+ # person.pets
489
+ # # => [
490
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
491
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
492
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
493
+ # # ]
494
+ #
495
+ # person.pets.delete_all
496
+ #
497
+ # Pet.find(1, 2, 3)
498
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
499
+ def delete_all
500
+ @association.delete_all
501
+ end
502
+
503
+ # Deletes the records of the collection directly from the database
504
+ # ignoring the +:dependent+ option. Records are instantiated and it
505
+ # invokes +before_remove+, +after_remove+ , +before_destroy+ and
506
+ # +after_destroy+ callbacks.
507
+ #
508
+ # class Person < ActiveRecord::Base
509
+ # has_many :pets
510
+ # end
511
+ #
512
+ # person.pets.size # => 3
513
+ # person.pets
514
+ # # => [
515
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
516
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
517
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
518
+ # # ]
519
+ #
520
+ # person.pets.destroy_all
521
+ #
522
+ # person.pets.size # => 0
523
+ # person.pets # => []
524
+ #
525
+ # Pet.find(1) # => Couldn't find Pet with id=1
526
+ def destroy_all
527
+ @association.destroy_all
528
+ end
529
+
530
+ # Deletes the +records+ supplied from the collection according to the strategy
531
+ # specified by the +:dependent+ option. If no +:dependent+ option is given,
532
+ # then it will follow the default strategy. Returns an array with the
533
+ # deleted records.
534
+ #
535
+ # For <tt>has_many :through</tt> associations, the default deletion strategy is
536
+ # +:delete_all+.
537
+ #
538
+ # For +has_many+ associations, the default deletion strategy is +:nullify+.
539
+ # This sets the foreign keys to +NULL+.
540
+ #
541
+ # class Person < ActiveRecord::Base
542
+ # has_many :pets # dependent: :nullify option by default
543
+ # end
544
+ #
545
+ # person.pets.size # => 3
546
+ # person.pets
547
+ # # => [
548
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
549
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
550
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
551
+ # # ]
552
+ #
553
+ # person.pets.delete(Pet.find(1))
554
+ # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
555
+ #
556
+ # person.pets.size # => 2
557
+ # person.pets
558
+ # # => [
559
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
560
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
561
+ # # ]
562
+ #
563
+ # Pet.find(1)
564
+ # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>
565
+ #
566
+ # If it is set to <tt>:destroy</tt> all the +records+ are removed by calling
567
+ # their +destroy+ method. See +destroy+ for more information.
568
+ #
569
+ # class Person < ActiveRecord::Base
570
+ # has_many :pets, dependent: :destroy
571
+ # end
572
+ #
573
+ # person.pets.size # => 3
574
+ # person.pets
575
+ # # => [
576
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
577
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
578
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
579
+ # # ]
580
+ #
581
+ # person.pets.delete(Pet.find(1), Pet.find(3))
582
+ # # => [
583
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
584
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
585
+ # # ]
586
+ #
587
+ # person.pets.size # => 1
588
+ # person.pets
589
+ # # => [#<Pet id: 2, name: "Spook", person_id: 1>]
590
+ #
591
+ # Pet.find(1, 3)
592
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 3)
593
+ #
594
+ # If it is set to <tt>:delete_all</tt>, all the +records+ are deleted
595
+ # *without* calling their +destroy+ method.
596
+ #
597
+ # class Person < ActiveRecord::Base
598
+ # has_many :pets, dependent: :delete_all
599
+ # end
600
+ #
601
+ # person.pets.size # => 3
602
+ # person.pets
603
+ # # => [
604
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
605
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
606
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
607
+ # # ]
608
+ #
609
+ # person.pets.delete(Pet.find(1))
610
+ # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
611
+ #
612
+ # person.pets.size # => 2
613
+ # person.pets
614
+ # # => [
615
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
616
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
617
+ # # ]
618
+ #
619
+ # Pet.find(1)
620
+ # # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=1
621
+ #
622
+ # You can pass +Integer+ or +String+ values, it finds the records
623
+ # responding to the +id+ and executes delete on them.
624
+ #
625
+ # class Person < ActiveRecord::Base
626
+ # has_many :pets
627
+ # end
628
+ #
629
+ # person.pets.size # => 3
630
+ # person.pets
631
+ # # => [
632
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
633
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
634
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
635
+ # # ]
636
+ #
637
+ # person.pets.delete("1")
638
+ # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
639
+ #
640
+ # person.pets.delete(2, 3)
641
+ # # => [
642
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
643
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
644
+ # # ]
645
+ def delete(*records)
646
+ @association.delete(*records)
647
+ end
648
+
649
+ # Destroys the +records+ supplied and removes them from the collection.
650
+ # This method will _always_ remove record from the database ignoring
651
+ # the +:dependent+ option. Returns an array with the removed records.
652
+ #
653
+ # class Person < ActiveRecord::Base
654
+ # has_many :pets
655
+ # end
656
+ #
657
+ # person.pets.size # => 3
658
+ # person.pets
659
+ # # => [
660
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
661
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
662
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
663
+ # # ]
664
+ #
665
+ # person.pets.destroy(Pet.find(1))
666
+ # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
667
+ #
668
+ # person.pets.size # => 2
669
+ # person.pets
670
+ # # => [
671
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
672
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
673
+ # # ]
674
+ #
675
+ # person.pets.destroy(Pet.find(2), Pet.find(3))
676
+ # # => [
677
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
678
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
679
+ # # ]
680
+ #
681
+ # person.pets.size # => 0
682
+ # person.pets # => []
683
+ #
684
+ # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
685
+ #
686
+ # You can pass +Integer+ or +String+ values, it finds the records
687
+ # responding to the +id+ and then deletes them from the database.
688
+ #
689
+ # person.pets.size # => 3
690
+ # person.pets
691
+ # # => [
692
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
693
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
694
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
695
+ # # ]
696
+ #
697
+ # person.pets.destroy("4")
698
+ # # => #<Pet id: 4, name: "Benny", person_id: 1>
699
+ #
700
+ # person.pets.size # => 2
701
+ # person.pets
702
+ # # => [
703
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
704
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
705
+ # # ]
706
+ #
707
+ # person.pets.destroy(5, 6)
708
+ # # => [
709
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
710
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
711
+ # # ]
712
+ #
713
+ # person.pets.size # => 0
714
+ # person.pets # => []
715
+ #
716
+ # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6)
717
+ def destroy(*records)
718
+ @association.destroy(*records)
719
+ end
720
+
721
+ ##
722
+ # :method: distinct
723
+ #
724
+ # :call-seq:
725
+ # distinct(value = true)
726
+ #
727
+ # Specifies whether the records should be unique or not.
728
+ #
729
+ # class Person < ActiveRecord::Base
730
+ # has_many :pets
731
+ # end
732
+ #
733
+ # person.pets.select(:name)
734
+ # # => [
735
+ # # #<Pet name: "Fancy-Fancy">,
736
+ # # #<Pet name: "Fancy-Fancy">
737
+ # # ]
738
+ #
739
+ # person.pets.select(:name).distinct
740
+ # # => [#<Pet name: "Fancy-Fancy">]
741
+ #
742
+ # person.pets.select(:name).distinct.distinct(false)
743
+ # # => [
744
+ # # #<Pet name: "Fancy-Fancy">,
745
+ # # #<Pet name: "Fancy-Fancy">
746
+ # # ]
747
+
748
+ #--
749
+ def uniq
750
+ load_target.uniq
751
+ end
752
+
753
+ def calculate(operation, column_name)
754
+ null_scope? ? scope.calculate(operation, column_name) : super
755
+ end
756
+
757
+ def pluck(*column_names)
758
+ null_scope? ? scope.pluck(*column_names) : super
759
+ end
760
+
761
+ ##
762
+ # :method: count
763
+ #
764
+ # :call-seq:
765
+ # count(column_name = nil, &block)
766
+ #
767
+ # Count all records.
768
+ #
769
+ # class Person < ActiveRecord::Base
770
+ # has_many :pets
771
+ # end
772
+ #
773
+ # # This will perform the count using SQL.
774
+ # person.pets.count # => 3
775
+ # person.pets
776
+ # # => [
777
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
778
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
779
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
780
+ # # ]
781
+ #
782
+ # Passing a block will select all of a person's pets in SQL and then
783
+ # perform the count using Ruby.
784
+ #
785
+ # person.pets.count { |pet| pet.name.include?('-') } # => 2
786
+
787
+ # Returns the size of the collection. If the collection hasn't been loaded,
788
+ # it executes a <tt>SELECT COUNT(*)</tt> query. Else it calls <tt>collection.size</tt>.
789
+ #
790
+ # If the collection has been already loaded +size+ and +length+ are
791
+ # equivalent. If not and you are going to need the records anyway
792
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
793
+ #
794
+ # class Person < ActiveRecord::Base
795
+ # has_many :pets
796
+ # end
797
+ #
798
+ # person.pets.size # => 3
799
+ # # executes something like SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" = 1
800
+ #
801
+ # person.pets # This will execute a SELECT * FROM query
802
+ # # => [
803
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
804
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
805
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
806
+ # # ]
807
+ #
808
+ # person.pets.size # => 3
809
+ # # Because the collection is already loaded, this will behave like
810
+ # # collection.size and no SQL count query is executed.
811
+ def size
812
+ @association.size
813
+ end
814
+
815
+ ##
816
+ # :method: length
817
+ #
818
+ # :call-seq:
819
+ # length()
820
+ #
821
+ # Returns the size of the collection calling +size+ on the target.
822
+ # If the collection has been already loaded, +length+ and +size+ are
823
+ # equivalent. If not and you are going to need the records anyway this
824
+ # method will take one less query. Otherwise +size+ is more efficient.
825
+ #
826
+ # class Person < ActiveRecord::Base
827
+ # has_many :pets
828
+ # end
829
+ #
830
+ # person.pets.length # => 3
831
+ # # executes something like SELECT "pets".* FROM "pets" WHERE "pets"."person_id" = 1
832
+ #
833
+ # # Because the collection is loaded, you can
834
+ # # call the collection with no additional queries:
835
+ # person.pets
836
+ # # => [
837
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
838
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
839
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
840
+ # # ]
841
+
842
+ # Returns +true+ if the collection is empty. If the collection has been
843
+ # loaded it is equivalent
844
+ # to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
845
+ # it is equivalent to <tt>!collection.exists?</tt>. If the collection has
846
+ # not already been loaded and you are going to fetch the records anyway it
847
+ # is better to check <tt>collection.length.zero?</tt>.
848
+ #
849
+ # class Person < ActiveRecord::Base
850
+ # has_many :pets
851
+ # end
852
+ #
853
+ # person.pets.count # => 1
854
+ # person.pets.empty? # => false
855
+ #
856
+ # person.pets.delete_all
857
+ #
858
+ # person.pets.count # => 0
859
+ # person.pets.empty? # => true
860
+ def empty?
861
+ @association.empty?
862
+ end
863
+
864
+ ##
865
+ # :method: any?
866
+ #
867
+ # :call-seq:
868
+ # any?()
869
+ #
870
+ # Returns +true+ if the collection is not empty.
871
+ #
872
+ # class Person < ActiveRecord::Base
873
+ # has_many :pets
874
+ # end
875
+ #
876
+ # person.pets.count # => 0
877
+ # person.pets.any? # => false
878
+ #
879
+ # person.pets << Pet.new(name: 'Snoop')
880
+ # person.pets.count # => 1
881
+ # person.pets.any? # => true
882
+ #
883
+ # You can also pass a +block+ to define criteria. The behavior
884
+ # is the same, it returns true if the collection based on the
885
+ # criteria is not empty.
886
+ #
887
+ # person.pets
888
+ # # => [#<Pet name: "Snoop", group: "dogs">]
889
+ #
890
+ # person.pets.any? do |pet|
891
+ # pet.group == 'cats'
892
+ # end
893
+ # # => false
894
+ #
895
+ # person.pets.any? do |pet|
896
+ # pet.group == 'dogs'
897
+ # end
898
+ # # => true
899
+
900
+ ##
901
+ # :method: many?
902
+ #
903
+ # :call-seq:
904
+ # many?()
905
+ #
906
+ # Returns true if the collection has more than one record.
907
+ # Equivalent to <tt>collection.size > 1</tt>.
908
+ #
909
+ # class Person < ActiveRecord::Base
910
+ # has_many :pets
911
+ # end
912
+ #
913
+ # person.pets.count # => 1
914
+ # person.pets.many? # => false
915
+ #
916
+ # person.pets << Pet.new(name: 'Snoopy')
917
+ # person.pets.count # => 2
918
+ # person.pets.many? # => true
919
+ #
920
+ # You can also pass a +block+ to define criteria. The
921
+ # behavior is the same, it returns true if the collection
922
+ # based on the criteria has more than one record.
923
+ #
924
+ # person.pets
925
+ # # => [
926
+ # # #<Pet name: "Gorby", group: "cats">,
927
+ # # #<Pet name: "Puff", group: "cats">,
928
+ # # #<Pet name: "Snoop", group: "dogs">
929
+ # # ]
930
+ #
931
+ # person.pets.many? do |pet|
932
+ # pet.group == 'dogs'
933
+ # end
934
+ # # => false
935
+ #
936
+ # person.pets.many? do |pet|
937
+ # pet.group == 'cats'
938
+ # end
939
+ # # => true
940
+
941
+ # Returns +true+ if the given +record+ is present in the collection.
942
+ #
943
+ # class Person < ActiveRecord::Base
944
+ # has_many :pets
945
+ # end
946
+ #
947
+ # person.pets # => [#<Pet id: 20, name: "Snoop">]
948
+ #
949
+ # person.pets.include?(Pet.find(20)) # => true
950
+ # person.pets.include?(Pet.find(21)) # => false
951
+ def include?(record)
952
+ !!@association.include?(record)
953
+ end
954
+
955
+ def proxy_association
956
+ @association
957
+ end
958
+
959
+ # Returns a <tt>Relation</tt> object for the records in this association
960
+ def scope
961
+ @scope ||= @association.scope
962
+ end
963
+
964
+ # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
965
+ # contain the same number of elements and if each element is equal
966
+ # to the corresponding element in the +other+ array, otherwise returns
967
+ # +false+.
968
+ #
969
+ # class Person < ActiveRecord::Base
970
+ # has_many :pets
971
+ # end
972
+ #
973
+ # person.pets
974
+ # # => [
975
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
976
+ # # #<Pet id: 2, name: "Spook", person_id: 1>
977
+ # # ]
978
+ #
979
+ # other = person.pets.to_ary
980
+ #
981
+ # person.pets == other
982
+ # # => true
983
+ #
984
+ # other = [Pet.new(id: 1), Pet.new(id: 2)]
985
+ #
986
+ # person.pets == other
987
+ # # => false
988
+ def ==(other)
989
+ load_target == other
990
+ end
991
+
992
+ # Returns a new array of objects from the collection. If the collection
993
+ # hasn't been loaded, it fetches the records from the database.
994
+ #
995
+ # class Person < ActiveRecord::Base
996
+ # has_many :pets
997
+ # end
998
+ #
999
+ # person.pets
1000
+ # # => [
1001
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
1002
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
1003
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
1004
+ # # ]
1005
+ #
1006
+ # other_pets = person.pets.to_ary
1007
+ # # => [
1008
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
1009
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
1010
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
1011
+ # # ]
1012
+ #
1013
+ # other_pets.replace([Pet.new(name: 'BooGoo')])
1014
+ #
1015
+ # other_pets
1016
+ # # => [#<Pet id: nil, name: "BooGoo", person_id: 1>]
1017
+ #
1018
+ # person.pets
1019
+ # # This is not affected by replace
1020
+ # # => [
1021
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
1022
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
1023
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
1024
+ # # ]
1025
+ def to_ary
1026
+ load_target.dup
1027
+ end
1028
+ alias_method :to_a, :to_ary
1029
+
1030
+ def records # :nodoc:
1031
+ load_target
1032
+ end
1033
+
1034
+ # Adds one or more +records+ to the collection by setting their foreign keys
1035
+ # to the association's primary key. Returns +self+, so several appends may be
1036
+ # chained together.
1037
+ #
1038
+ # class Person < ActiveRecord::Base
1039
+ # has_many :pets
1040
+ # end
1041
+ #
1042
+ # person.pets.size # => 0
1043
+ # person.pets << Pet.new(name: 'Fancy-Fancy')
1044
+ # person.pets << [Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')]
1045
+ # person.pets.size # => 3
1046
+ #
1047
+ # person.id # => 1
1048
+ # person.pets
1049
+ # # => [
1050
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
1051
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
1052
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
1053
+ # # ]
1054
+ def <<(*records)
1055
+ proxy_association.concat(records) && self
1056
+ end
1057
+ alias_method :push, :<<
1058
+ alias_method :append, :<<
1059
+
1060
+ def prepend(*args)
1061
+ raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
1062
+ end
1063
+
1064
+ # Equivalent to +delete_all+. The difference is that returns +self+, instead
1065
+ # of an array with the deleted objects, so methods can be chained. See
1066
+ # +delete_all+ for more information.
1067
+ # Note that because +delete_all+ removes records by directly
1068
+ # running an SQL query into the database, the +updated_at+ column of
1069
+ # the object is not changed.
1070
+ def clear
1071
+ delete_all
1072
+ self
1073
+ end
1074
+
1075
+ # Reloads the collection from the database. Returns +self+.
1076
+ # Equivalent to <tt>collection(true)</tt>.
1077
+ #
1078
+ # class Person < ActiveRecord::Base
1079
+ # has_many :pets
1080
+ # end
1081
+ #
1082
+ # person.pets # fetches pets from the database
1083
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
1084
+ #
1085
+ # person.pets # uses the pets cache
1086
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
1087
+ #
1088
+ # person.pets.reload # fetches pets from the database
1089
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
1090
+ #
1091
+ # person.pets(true) # fetches pets from the database
1092
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
1093
+ def reload
1094
+ proxy_association.reload
1095
+ reset_scope
1096
+ end
1097
+
1098
+ # Unloads the association. Returns +self+.
1099
+ #
1100
+ # class Person < ActiveRecord::Base
1101
+ # has_many :pets
1102
+ # end
1103
+ #
1104
+ # person.pets # fetches pets from the database
1105
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
1106
+ #
1107
+ # person.pets # uses the pets cache
1108
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
1109
+ #
1110
+ # person.pets.reset # clears the pets cache
1111
+ #
1112
+ # person.pets # fetches pets from the database
1113
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
1114
+ def reset
1115
+ proxy_association.reset
1116
+ proxy_association.reset_scope
1117
+ reset_scope
1118
+ end
1119
+
1120
+ def reset_scope # :nodoc:
1121
+ @offsets = {}
1122
+ @scope = nil
1123
+ self
1124
+ end
1125
+
1126
+ delegate_methods = [
1127
+ ActiveRecord::QueryMethods,
1128
+ ActiveRecord::SpawnMethods,
1129
+ ].flat_map { |klass|
1130
+ klass.public_instance_methods(false)
1131
+ } - self.public_instance_methods(false) - [:select] + [:scoping]
1132
+
1133
+ delegate(*delegate_methods, to: :scope)
1134
+
1135
+ private
1136
+
1137
+ def find_nth_with_limit(index, limit)
1138
+ load_target if find_from_target?
1139
+ super
1140
+ end
1141
+
1142
+ def find_nth_from_last(index)
1143
+ load_target if find_from_target?
1144
+ super
1145
+ end
1146
+
1147
+ def null_scope?
1148
+ @association.null_scope?
1149
+ end
1150
+
1151
+ def find_from_target?
1152
+ @association.find_from_target?
1153
+ end
1154
+
1155
+ def exec_queries
1156
+ load_target
1157
+ end
1158
+ end
1159
+ end
1160
+ end