duck_record 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,894 @@
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
31
+ include Enumerable
32
+
33
+ delegate :to_xml, :encode_with, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join,
34
+ :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of,
35
+ :to_sentence, :to_formatted_s,
36
+ :shuffle, :split, :index, to: :records
37
+
38
+ attr_reader :klass
39
+ alias :model :klass
40
+
41
+ def initialize(klass, association) #:nodoc:
42
+ @klass = klass
43
+ @association = association
44
+ end
45
+
46
+ def target
47
+ @association.target
48
+ end
49
+
50
+ ##
51
+ # :method: first
52
+ #
53
+ # :call-seq:
54
+ # first(limit = nil)
55
+ #
56
+ # Returns the first record, or the first +n+ records, from the collection.
57
+ # If the collection is empty, the first form returns +nil+, and the second
58
+ # form returns an empty array.
59
+ #
60
+ # class Person < ActiveRecord::Base
61
+ # has_many :pets
62
+ # end
63
+ #
64
+ # person.pets
65
+ # # => [
66
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
67
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
68
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
69
+ # # ]
70
+ #
71
+ # person.pets.first # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
72
+ #
73
+ # person.pets.first(2)
74
+ # # => [
75
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
76
+ # # #<Pet id: 2, name: "Spook", person_id: 1>
77
+ # # ]
78
+ #
79
+ # another_person_without.pets # => []
80
+ # another_person_without.pets.first # => nil
81
+ # another_person_without.pets.first(3) # => []
82
+
83
+ ##
84
+ # :method: second
85
+ #
86
+ # :call-seq:
87
+ # second()
88
+ #
89
+ # Same as #first except returns only the second record.
90
+
91
+ ##
92
+ # :method: third
93
+ #
94
+ # :call-seq:
95
+ # third()
96
+ #
97
+ # Same as #first except returns only the third record.
98
+
99
+ ##
100
+ # :method: fourth
101
+ #
102
+ # :call-seq:
103
+ # fourth()
104
+ #
105
+ # Same as #first except returns only the fourth record.
106
+
107
+ ##
108
+ # :method: fifth
109
+ #
110
+ # :call-seq:
111
+ # fifth()
112
+ #
113
+ # Same as #first except returns only the fifth record.
114
+
115
+ ##
116
+ # :method: forty_two
117
+ #
118
+ # :call-seq:
119
+ # forty_two()
120
+ #
121
+ # Same as #first except returns only the forty second record.
122
+ # Also known as accessing "the reddit".
123
+
124
+ ##
125
+ # :method: third_to_last
126
+ #
127
+ # :call-seq:
128
+ # third_to_last()
129
+ #
130
+ # Same as #first except returns only the third-to-last record.
131
+
132
+ ##
133
+ # :method: second_to_last
134
+ #
135
+ # :call-seq:
136
+ # second_to_last()
137
+ #
138
+ # Same as #first except returns only the second-to-last record.
139
+
140
+ # Returns a new object of the collection type that has been instantiated
141
+ # with +attributes+ and linked to this object, but have not yet been saved.
142
+ # You can pass an array of attributes hashes, this will return an array
143
+ # with the new objects.
144
+ #
145
+ # class Person
146
+ # has_many :pets
147
+ # end
148
+ #
149
+ # person.pets.build
150
+ # # => #<Pet id: nil, name: nil, person_id: 1>
151
+ #
152
+ # person.pets.build(name: 'Fancy-Fancy')
153
+ # # => #<Pet id: nil, name: "Fancy-Fancy", person_id: 1>
154
+ #
155
+ # person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}])
156
+ # # => [
157
+ # # #<Pet id: nil, name: "Spook", person_id: 1>,
158
+ # # #<Pet id: nil, name: "Choo-Choo", person_id: 1>,
159
+ # # #<Pet id: nil, name: "Brain", person_id: 1>
160
+ # # ]
161
+ #
162
+ # person.pets.size # => 5 # size of the collection
163
+ # person.pets.count # => 0 # count from database
164
+ def build(attributes = {}, &block)
165
+ proxy_association.build(attributes, &block)
166
+ end
167
+ alias_method :new, :build
168
+
169
+ # Add one or more records to the collection by setting their foreign keys
170
+ # to the association's primary key. Since #<< flattens its argument list and
171
+ # inserts each record, +push+ and #concat behave identically. Returns +self+
172
+ # so method calls may be chained.
173
+ #
174
+ # class Person < ActiveRecord::Base
175
+ # has_many :pets
176
+ # end
177
+ #
178
+ # person.pets.size # => 0
179
+ # person.pets.concat(Pet.new(name: 'Fancy-Fancy'))
180
+ # person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo'))
181
+ # person.pets.size # => 3
182
+ #
183
+ # person.id # => 1
184
+ # person.pets
185
+ # # => [
186
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
187
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
188
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
189
+ # # ]
190
+ #
191
+ # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')])
192
+ # person.pets.size # => 5
193
+ def concat(*records)
194
+ proxy_association.concat(*records)
195
+ end
196
+
197
+ # Replaces this collection with +other_array+. This will perform a diff
198
+ # and delete/add only records that have changed.
199
+ #
200
+ # class Person < ActiveRecord::Base
201
+ # has_many :pets
202
+ # end
203
+ #
204
+ # person.pets
205
+ # # => [#<Pet id: 1, name: "Gorby", group: "cats", person_id: 1>]
206
+ #
207
+ # other_pets = [Pet.new(name: 'Puff', group: 'celebrities']
208
+ #
209
+ # person.pets.replace(other_pets)
210
+ #
211
+ # person.pets
212
+ # # => [#<Pet id: 2, name: "Puff", group: "celebrities", person_id: 1>]
213
+ #
214
+ # If the supplied array has an incorrect association type, it raises
215
+ # an <tt>ActiveRecord::AssociationTypeMismatch</tt> error:
216
+ #
217
+ # person.pets.replace(["doo", "ggie", "gaga"])
218
+ # # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String
219
+ def replace(other_array)
220
+ proxy_association.replace(other_array)
221
+ end
222
+
223
+ # Deletes all the records from the collection according to the strategy
224
+ # specified by the +:dependent+ option. If no +:dependent+ option is given,
225
+ # then it will follow the default strategy.
226
+ #
227
+ # For <tt>has_many :through</tt> associations, the default deletion strategy is
228
+ # +:delete_all+.
229
+ #
230
+ # For +has_many+ associations, the default deletion strategy is +:nullify+.
231
+ # This sets the foreign keys to +NULL+.
232
+ #
233
+ # class Person < ActiveRecord::Base
234
+ # has_many :pets # dependent: :nullify option by default
235
+ # end
236
+ #
237
+ # person.pets.size # => 3
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.delete_all
246
+ # # => [
247
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
248
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
249
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
250
+ # # ]
251
+ #
252
+ # person.pets.size # => 0
253
+ # person.pets # => []
254
+ #
255
+ # Pet.find(1, 2, 3)
256
+ # # => [
257
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>,
258
+ # # #<Pet id: 2, name: "Spook", person_id: nil>,
259
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: nil>
260
+ # # ]
261
+ #
262
+ # Both +has_many+ and <tt>has_many :through</tt> dependencies default to the
263
+ # +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+.
264
+ # Records are not instantiated and callbacks will not be fired.
265
+ #
266
+ # class Person < ActiveRecord::Base
267
+ # has_many :pets, dependent: :destroy
268
+ # end
269
+ #
270
+ # person.pets.size # => 3
271
+ # person.pets
272
+ # # => [
273
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
274
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
275
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
276
+ # # ]
277
+ #
278
+ # person.pets.delete_all
279
+ #
280
+ # Pet.find(1, 2, 3)
281
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
282
+ #
283
+ # If it is set to <tt>:delete_all</tt>, all the objects are deleted
284
+ # *without* calling their +destroy+ method.
285
+ #
286
+ # class Person < ActiveRecord::Base
287
+ # has_many :pets, dependent: :delete_all
288
+ # end
289
+ #
290
+ # person.pets.size # => 3
291
+ # person.pets
292
+ # # => [
293
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
294
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
295
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
296
+ # # ]
297
+ #
298
+ # person.pets.delete_all
299
+ #
300
+ # Pet.find(1, 2, 3)
301
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
302
+ def delete_all
303
+ proxy_association.delete_all
304
+ end
305
+
306
+ # Deletes the records of the collection directly from the database
307
+ # ignoring the +:dependent+ option. Records are instantiated and it
308
+ # invokes +before_remove+, +after_remove+ , +before_destroy+ and
309
+ # +after_destroy+ callbacks.
310
+ #
311
+ # class Person < ActiveRecord::Base
312
+ # has_many :pets
313
+ # end
314
+ #
315
+ # person.pets.size # => 3
316
+ # person.pets
317
+ # # => [
318
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
319
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
320
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
321
+ # # ]
322
+ #
323
+ # person.pets.destroy_all
324
+ #
325
+ # person.pets.size # => 0
326
+ # person.pets # => []
327
+ #
328
+ # Pet.find(1) # => Couldn't find Pet with id=1
329
+ def destroy_all
330
+ proxy_association.destroy_all
331
+ end
332
+
333
+ # Deletes the +records+ supplied from the collection according to the strategy
334
+ # specified by the +:dependent+ option. If no +:dependent+ option is given,
335
+ # then it will follow the default strategy. Returns an array with the
336
+ # deleted records.
337
+ #
338
+ # For <tt>has_many :through</tt> associations, the default deletion strategy is
339
+ # +:delete_all+.
340
+ #
341
+ # For +has_many+ associations, the default deletion strategy is +:nullify+.
342
+ # This sets the foreign keys to +NULL+.
343
+ #
344
+ # class Person < ActiveRecord::Base
345
+ # has_many :pets # dependent: :nullify option by default
346
+ # end
347
+ #
348
+ # person.pets.size # => 3
349
+ # person.pets
350
+ # # => [
351
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
352
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
353
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
354
+ # # ]
355
+ #
356
+ # person.pets.delete(Pet.find(1))
357
+ # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
358
+ #
359
+ # person.pets.size # => 2
360
+ # person.pets
361
+ # # => [
362
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
363
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
364
+ # # ]
365
+ #
366
+ # Pet.find(1)
367
+ # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>
368
+ #
369
+ # If it is set to <tt>:destroy</tt> all the +records+ are removed by calling
370
+ # their +destroy+ method. See +destroy+ for more information.
371
+ #
372
+ # class Person < ActiveRecord::Base
373
+ # has_many :pets, dependent: :destroy
374
+ # end
375
+ #
376
+ # person.pets.size # => 3
377
+ # person.pets
378
+ # # => [
379
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
380
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
381
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
382
+ # # ]
383
+ #
384
+ # person.pets.delete(Pet.find(1), Pet.find(3))
385
+ # # => [
386
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
387
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
388
+ # # ]
389
+ #
390
+ # person.pets.size # => 1
391
+ # person.pets
392
+ # # => [#<Pet id: 2, name: "Spook", person_id: 1>]
393
+ #
394
+ # Pet.find(1, 3)
395
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 3)
396
+ #
397
+ # If it is set to <tt>:delete_all</tt>, all the +records+ are deleted
398
+ # *without* calling their +destroy+ method.
399
+ #
400
+ # class Person < ActiveRecord::Base
401
+ # has_many :pets, dependent: :delete_all
402
+ # end
403
+ #
404
+ # person.pets.size # => 3
405
+ # person.pets
406
+ # # => [
407
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
408
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
409
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
410
+ # # ]
411
+ #
412
+ # person.pets.delete(Pet.find(1))
413
+ # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
414
+ #
415
+ # person.pets.size # => 2
416
+ # person.pets
417
+ # # => [
418
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
419
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
420
+ # # ]
421
+ #
422
+ # Pet.find(1)
423
+ # # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=1
424
+ #
425
+ # You can pass +Integer+ or +String+ values, it finds the records
426
+ # responding to the +id+ and executes delete on them.
427
+ #
428
+ # class Person < ActiveRecord::Base
429
+ # has_many :pets
430
+ # end
431
+ #
432
+ # person.pets.size # => 3
433
+ # person.pets
434
+ # # => [
435
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
436
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
437
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
438
+ # # ]
439
+ #
440
+ # person.pets.delete("1")
441
+ # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
442
+ #
443
+ # person.pets.delete(2, 3)
444
+ # # => [
445
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
446
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
447
+ # # ]
448
+ def delete(*records)
449
+ proxy_association.delete(*records)
450
+ end
451
+
452
+ # Destroys the +records+ supplied and removes them from the collection.
453
+ # This method will _always_ remove record from the database ignoring
454
+ # the +:dependent+ option. Returns an array with the removed records.
455
+ #
456
+ # class Person < ActiveRecord::Base
457
+ # has_many :pets
458
+ # end
459
+ #
460
+ # person.pets.size # => 3
461
+ # person.pets
462
+ # # => [
463
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
464
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
465
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
466
+ # # ]
467
+ #
468
+ # person.pets.destroy(Pet.find(1))
469
+ # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
470
+ #
471
+ # person.pets.size # => 2
472
+ # person.pets
473
+ # # => [
474
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
475
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
476
+ # # ]
477
+ #
478
+ # person.pets.destroy(Pet.find(2), Pet.find(3))
479
+ # # => [
480
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
481
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
482
+ # # ]
483
+ #
484
+ # person.pets.size # => 0
485
+ # person.pets # => []
486
+ #
487
+ # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
488
+ #
489
+ # You can pass +Integer+ or +String+ values, it finds the records
490
+ # responding to the +id+ and then deletes them from the database.
491
+ #
492
+ # person.pets.size # => 3
493
+ # person.pets
494
+ # # => [
495
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
496
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
497
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
498
+ # # ]
499
+ #
500
+ # person.pets.destroy("4")
501
+ # # => #<Pet id: 4, name: "Benny", person_id: 1>
502
+ #
503
+ # person.pets.size # => 2
504
+ # person.pets
505
+ # # => [
506
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
507
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
508
+ # # ]
509
+ #
510
+ # person.pets.destroy(5, 6)
511
+ # # => [
512
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
513
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
514
+ # # ]
515
+ #
516
+ # person.pets.size # => 0
517
+ # person.pets # => []
518
+ #
519
+ # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6)
520
+ def destroy(*records)
521
+ proxy_association.destroy(*records)
522
+ end
523
+
524
+ ##
525
+ # :method: distinct
526
+ #
527
+ # :call-seq:
528
+ # distinct(value = true)
529
+ #
530
+ # Specifies whether the records should be unique or not.
531
+ #
532
+ # class Person < ActiveRecord::Base
533
+ # has_many :pets
534
+ # end
535
+ #
536
+ # person.pets.select(:name)
537
+ # # => [
538
+ # # #<Pet name: "Fancy-Fancy">,
539
+ # # #<Pet name: "Fancy-Fancy">
540
+ # # ]
541
+ #
542
+ # person.pets.select(:name).distinct
543
+ # # => [#<Pet name: "Fancy-Fancy">]
544
+ #
545
+ # person.pets.select(:name).distinct.distinct(false)
546
+ # # => [
547
+ # # #<Pet name: "Fancy-Fancy">,
548
+ # # #<Pet name: "Fancy-Fancy">
549
+ # # ]
550
+
551
+ #--
552
+ def uniq
553
+ proxy_association.uniq
554
+ end
555
+
556
+ ##
557
+ # :method: count
558
+ #
559
+ # :call-seq:
560
+ # count(column_name = nil, &block)
561
+ #
562
+ # Count all records.
563
+ #
564
+ # class Person < ActiveRecord::Base
565
+ # has_many :pets
566
+ # end
567
+ #
568
+ # # This will perform the count using SQL.
569
+ # person.pets.count # => 3
570
+ # person.pets
571
+ # # => [
572
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
573
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
574
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
575
+ # # ]
576
+ #
577
+ # Passing a block will select all of a person's pets in SQL and then
578
+ # perform the count using Ruby.
579
+ #
580
+ # person.pets.count { |pet| pet.name.include?('-') } # => 2
581
+
582
+ # Returns the size of the collection. If the collection hasn't been loaded,
583
+ # it executes a <tt>SELECT COUNT(*)</tt> query. Else it calls <tt>collection.size</tt>.
584
+ #
585
+ # If the collection has been already loaded +size+ and +length+ are
586
+ # equivalent. If not and you are going to need the records anyway
587
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
588
+ #
589
+ # class Person < ActiveRecord::Base
590
+ # has_many :pets
591
+ # end
592
+ #
593
+ # person.pets.size # => 3
594
+ # # executes something like SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" = 1
595
+ #
596
+ # person.pets # This will execute a SELECT * FROM query
597
+ # # => [
598
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
599
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
600
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
601
+ # # ]
602
+ #
603
+ # person.pets.size # => 3
604
+ # # Because the collection is already loaded, this will behave like
605
+ # # collection.size and no SQL count query is executed.
606
+ def size
607
+ proxy_association.size
608
+ end
609
+
610
+ ##
611
+ # :method: length
612
+ #
613
+ # :call-seq:
614
+ # length()
615
+ #
616
+ # Returns the size of the collection calling +size+ on the target.
617
+ # If the collection has been already loaded, +length+ and +size+ are
618
+ # equivalent. If not and you are going to need the records anyway this
619
+ # method will take one less query. Otherwise +size+ is more efficient.
620
+ #
621
+ # class Person < ActiveRecord::Base
622
+ # has_many :pets
623
+ # end
624
+ #
625
+ # person.pets.length # => 3
626
+ # # executes something like SELECT "pets".* FROM "pets" WHERE "pets"."person_id" = 1
627
+ #
628
+ # # Because the collection is loaded, you can
629
+ # # call the collection with no additional queries:
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
+ # Returns +true+ if the collection is empty. If the collection has been
638
+ # loaded it is equivalent
639
+ # to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
640
+ # it is equivalent to <tt>!collection.exists?</tt>. If the collection has
641
+ # not already been loaded and you are going to fetch the records anyway it
642
+ # is better to check <tt>collection.length.zero?</tt>.
643
+ #
644
+ # class Person < ActiveRecord::Base
645
+ # has_many :pets
646
+ # end
647
+ #
648
+ # person.pets.count # => 1
649
+ # person.pets.empty? # => false
650
+ #
651
+ # person.pets.delete_all
652
+ #
653
+ # person.pets.count # => 0
654
+ # person.pets.empty? # => true
655
+ def empty?
656
+ proxy_association.empty?
657
+ end
658
+
659
+ ##
660
+ # :method: any?
661
+ #
662
+ # :call-seq:
663
+ # any?()
664
+ #
665
+ # Returns +true+ if the collection is not empty.
666
+ #
667
+ # class Person < ActiveRecord::Base
668
+ # has_many :pets
669
+ # end
670
+ #
671
+ # person.pets.count # => 0
672
+ # person.pets.any? # => false
673
+ #
674
+ # person.pets << Pet.new(name: 'Snoop')
675
+ # person.pets.count # => 1
676
+ # person.pets.any? # => true
677
+ #
678
+ # You can also pass a +block+ to define criteria. The behavior
679
+ # is the same, it returns true if the collection based on the
680
+ # criteria is not empty.
681
+ #
682
+ # person.pets
683
+ # # => [#<Pet name: "Snoop", group: "dogs">]
684
+ #
685
+ # person.pets.any? do |pet|
686
+ # pet.group == 'cats'
687
+ # end
688
+ # # => false
689
+ #
690
+ # person.pets.any? do |pet|
691
+ # pet.group == 'dogs'
692
+ # end
693
+ # # => true
694
+
695
+ ##
696
+ # :method: many?
697
+ #
698
+ # :call-seq:
699
+ # many?()
700
+ #
701
+ # Returns true if the collection has more than one record.
702
+ # Equivalent to <tt>collection.size > 1</tt>.
703
+ #
704
+ # class Person < ActiveRecord::Base
705
+ # has_many :pets
706
+ # end
707
+ #
708
+ # person.pets.count # => 1
709
+ # person.pets.many? # => false
710
+ #
711
+ # person.pets << Pet.new(name: 'Snoopy')
712
+ # person.pets.count # => 2
713
+ # person.pets.many? # => true
714
+ #
715
+ # You can also pass a +block+ to define criteria. The
716
+ # behavior is the same, it returns true if the collection
717
+ # based on the criteria has more than one record.
718
+ #
719
+ # person.pets
720
+ # # => [
721
+ # # #<Pet name: "Gorby", group: "cats">,
722
+ # # #<Pet name: "Puff", group: "cats">,
723
+ # # #<Pet name: "Snoop", group: "dogs">
724
+ # # ]
725
+ #
726
+ # person.pets.many? do |pet|
727
+ # pet.group == 'dogs'
728
+ # end
729
+ # # => false
730
+ #
731
+ # person.pets.many? do |pet|
732
+ # pet.group == 'cats'
733
+ # end
734
+ # # => true
735
+
736
+ # Returns +true+ if the given +record+ is present in the collection.
737
+ #
738
+ # class Person < ActiveRecord::Base
739
+ # has_many :pets
740
+ # end
741
+ #
742
+ # person.pets # => [#<Pet id: 20, name: "Snoop">]
743
+ #
744
+ # person.pets.include?(Pet.find(20)) # => true
745
+ # person.pets.include?(Pet.find(21)) # => false
746
+ def include?(record)
747
+ !!proxy_association.include?(record)
748
+ end
749
+
750
+ def proxy_association
751
+ @association
752
+ end
753
+
754
+ # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
755
+ # contain the same number of elements and if each element is equal
756
+ # to the corresponding element in the +other+ array, otherwise returns
757
+ # +false+.
758
+ #
759
+ # class Person < ActiveRecord::Base
760
+ # has_many :pets
761
+ # end
762
+ #
763
+ # person.pets
764
+ # # => [
765
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
766
+ # # #<Pet id: 2, name: "Spook", person_id: 1>
767
+ # # ]
768
+ #
769
+ # other = person.pets.to_ary
770
+ #
771
+ # person.pets == other
772
+ # # => true
773
+ #
774
+ # other = [Pet.new(id: 1), Pet.new(id: 2)]
775
+ #
776
+ # person.pets == other
777
+ # # => false
778
+ def ==(other)
779
+ proxy_association.target == other
780
+ end
781
+
782
+ # Returns a new array of objects from the collection. If the collection
783
+ # hasn't been loaded, it fetches the records from the database.
784
+ #
785
+ # class Person < ActiveRecord::Base
786
+ # has_many :pets
787
+ # end
788
+ #
789
+ # person.pets
790
+ # # => [
791
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
792
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
793
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
794
+ # # ]
795
+ #
796
+ # other_pets = person.pets.to_ary
797
+ # # => [
798
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
799
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
800
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
801
+ # # ]
802
+ #
803
+ # other_pets.replace([Pet.new(name: 'BooGoo')])
804
+ #
805
+ # other_pets
806
+ # # => [#<Pet id: nil, name: "BooGoo", person_id: 1>]
807
+ #
808
+ # person.pets
809
+ # # This is not affected by replace
810
+ # # => [
811
+ # # #<Pet id: 4, name: "Benny", person_id: 1>,
812
+ # # #<Pet id: 5, name: "Brain", person_id: 1>,
813
+ # # #<Pet id: 6, name: "Boss", person_id: 1>
814
+ # # ]
815
+ def to_ary
816
+ proxy_association.target.dup
817
+ end
818
+ alias_method :to_a, :to_ary
819
+
820
+ def records # :nodoc:
821
+ proxy_association.target
822
+ end
823
+
824
+ # Adds one or more +records+ to the collection by setting their foreign keys
825
+ # to the association's primary key. Returns +self+, so several appends may be
826
+ # chained together.
827
+ #
828
+ # class Person < ActiveRecord::Base
829
+ # has_many :pets
830
+ # end
831
+ #
832
+ # person.pets.size # => 0
833
+ # person.pets << Pet.new(name: 'Fancy-Fancy')
834
+ # person.pets << [Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')]
835
+ # person.pets.size # => 3
836
+ #
837
+ # person.id # => 1
838
+ # person.pets
839
+ # # => [
840
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
841
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
842
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
843
+ # # ]
844
+ def <<(*records)
845
+ proxy_association.concat(records) && self
846
+ end
847
+ alias_method :push, :<<
848
+ alias_method :append, :<<
849
+
850
+ def prepend(*_args)
851
+ raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
852
+ end
853
+
854
+ # Equivalent to +delete_all+. The difference is that returns +self+, instead
855
+ # of an array with the deleted objects, so methods can be chained. See
856
+ # +delete_all+ for more information.
857
+ # Note that because +delete_all+ removes records by directly
858
+ # running an SQL query into the database, the +updated_at+ column of
859
+ # the object is not changed.
860
+ def clear
861
+ delete_all
862
+ self
863
+ end
864
+
865
+ # Unloads the association. Returns +self+.
866
+ #
867
+ # class Person < ActiveRecord::Base
868
+ # has_many :pets
869
+ # end
870
+ #
871
+ # person.pets # fetches pets from the database
872
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
873
+ #
874
+ # person.pets # uses the pets cache
875
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
876
+ #
877
+ # person.pets.reset # clears the pets cache
878
+ #
879
+ # person.pets # fetches pets from the database
880
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
881
+ def reset
882
+ proxy_association.reset
883
+ self
884
+ end
885
+
886
+ def inspect
887
+ entries = records.take(11).map!(&:inspect)
888
+ entries[10] = '...' if entries.size == 11
889
+
890
+ "#<#{self.class.name} [#{entries.join(', ')}]>"
891
+ end
892
+ end
893
+ end
894
+ end