collection_proxy.rb 12.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
module ActiveRecord
  module Associations
    # Association proxies in Active Record are middlemen between the object that
    # holds the association, known as the <tt>@owner</tt>, and the actual associated
    # object, known as the <tt>@target</tt>. The kind of association any proxy is
    # about is available in <tt>@reflection</tt>. That's an instance of the class
    # ActiveRecord::Reflection::AssociationReflection.
    #
    # For example, given
    #
    #   class Blog < ActiveRecord::Base
    #     has_many :posts
    #   end
    #
A
Akira Matsuda 已提交
15
    #   blog = Blog.first
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
    #
    # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
    # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
    # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
    #
    # This class has most of the basic instance methods removed, and delegates
    # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
    # corner case, it even removes the +class+ method and that's why you get
    #
    #   blog.posts.class # => Array
    #
    # though the object behind <tt>blog.posts</tt> is not an Array, but an
    # ActiveRecord::Associations::HasManyAssociation.
    #
    # The <tt>@target</tt> object is not \loaded until needed. For example,
    #
    #   blog.posts.count
    #
    # is computed directly through SQL and does not trigger by itself the
    # instantiation of the actual post records.
36
    class CollectionProxy < Relation
37
      delegate :target, :load_target, :loaded?, :to => :@association
38

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
      ##
      # :method: find
      # Finds an object in the collection responding to the +id+. Uses the same
      # rules as +ActiveRecord::Base.find+. Returns +ActiveRecord::RecordNotFound++
      # error if the object can not be found.
      #
      #   class Person < ActiveRecord::Base
      #     has_many :pets
      #   end
      #
      #   person.pets
      #   # => [
      #   #       #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
      #   #       #<Pet id: 2, name: "Spook", person_id: 1>,
      #   #       #<Pet id: 3, name: "Choo-Choo", person_id: 1>
      #   #    ]  
      #   
      #   person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
      #   person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4

59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
      ##
      # :method: first
      # Returns the first record, or the first +n+ records, from the collection.
      # If the collection is empty, the first form returns nil, and the second
      # form returns an empty array.
      #
      #   class Person < ActiveRecord::Base
      #     has_many :pets
      #   end
      #
      #   person.pets
      #   # => [
      #   #       #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
      #   #       #<Pet id: 2, name: "Spook", person_id: 1>,
      #   #       #<Pet id: 3, name: "Choo-Choo", person_id: 1>
V
Vijay Dev 已提交
74
      #   #    ]
75 76
      #
      #   person.pets.first # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
V
Vijay Dev 已提交
77
      #
78 79 80 81 82 83 84 85 86 87
      #   person.pets.first(2)
      #   # => [
      #   #      #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
      #   #      #<Pet id: 2, name: "Spook", person_id: 1>
      #   #    ]
      #
      #   another_person_without.pets          # => []
      #   another_person_without.pets.first    # => nil
      #   another_person_without.pets.first(3) # => []

88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
      ##
      # :method: last
      # Returns the last record, or the last +n+ records, from the collection.
      # If the collection is empty, the first form returns nil, and the second
      # form returns an empty array.
      #
      #   class Person < ActiveRecord::Base
      #     has_many :pets
      #   end
      #
      #   person.pets
      #   # => [
      #   #       #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
      #   #       #<Pet id: 2, name: "Spook", person_id: 1>,
      #   #       #<Pet id: 3, name: "Choo-Choo", person_id: 1>
V
Vijay Dev 已提交
103
      #   #    ]
104 105
      #
      #   person.pets.last # => #<Pet id: 3, name: "Choo-Choo", person_id: 1>
V
Vijay Dev 已提交
106
      #
107 108 109 110 111 112 113 114 115 116
      #   person.pets.last(2)
      #   # => [
      #   #      #<Pet id: 2, name: "Spook", person_id: 1>,
      #   #      #<Pet id: 3, name: "Choo-Choo", person_id: 1>
      #   #    ]
      #
      #   another_person_without.pets         # => []
      #   another_person_without.pets.last    # => nil
      #   another_person_without.pets.last(3) # => []

117 118
      ##
      # :method: concat
119 120 121 122
      # Add one or more records to the collection by setting their foreign keys
      # to the association's primary key. Since << flattens its argument list and
      # inserts each record, +push+ and +concat+ behave identically. Returns +self+
      # so method calls may be chained.
123 124 125 126 127
      #
      #   class Person < ActiveRecord::Base
      #     pets :has_many
      #   end
      #
128 129 130 131
      #   person.pets.size # => 0
      #   person.pets.concat(Pet.new(name: 'Fancy-Fancy'))
      #   person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo'))
      #   person.pets.size # => 3
132
      #
133 134 135 136 137 138 139 140 141 142
      #   person.id # => 1
      #   person.pets
      #   # => [
      #   #       #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
      #   #       #<Pet id: 2, name: "Spook", person_id: 1>,
      #   #       #<Pet id: 3, name: "Choo-Choo", person_id: 1>
      #   #    ]
      #
      #   person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')])
      #   person.pets.size # => 5
V
Vijay Dev 已提交
143

144 145 146 147 148 149 150 151 152 153
      ##
      # :method: replace
      # Replace this collection with +other_array+. This will perform a diff
      # and delete/add only records that have changed.
      #
      #   class Person < ActiveRecord::Base
      #     has_many :pets
      #   end
      #
      #   person.pets
V
Vijay Dev 已提交
154
      #   # => [#<Pet id: 1, name: "Gorby", group: "cats", person_id: 1>]
155
      #
V
Vijay Dev 已提交
156
      #   other_pets = [Pet.new(name: 'Puff', group: 'celebrities']
157 158 159 160
      #
      #   person.pets.replace(other_pets)
      #
      #   person.pets
V
Vijay Dev 已提交
161
      #   # => [#<Pet id: 2, name: "Puff", group: "celebrities", person_id: 1>]
162 163
      #
      # If the supplied array has an incorrect association type, it raises
V
Vijay Dev 已提交
164
      # an <tt>ActiveRecord::AssociationTypeMismatch</tt> error:
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
      #
      #   person.pets.replace(["doo", "ggie", "gaga"])
      #   # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String

      ##
      # :method: destroy_all
      # Destroy all the records from this association.
      #
      #   class Person < ActiveRecord::Base
      #     has_many :pets
      #   end
      #
      #   person.pets.size # => 3
      #
      #   person.pets.destroy_all
      #
      #   person.pets.size # => 0
      #   person.pets      # => []
V
Vijay Dev 已提交
183

184 185 186 187 188 189 190 191 192
      ##
      # :method: empty?
      # Returns true if the collection is empty.
      #
      #   class Person < ActiveRecord::Base
      #     has_many :pets
      #   end
      #
      #   person.pets.count  # => 1
V
Vijay Dev 已提交
193
      #   person.pets.empty? # => false
194 195
      #
      #   person.pets.delete_all
V
Vijay Dev 已提交
196
      #
197 198 199 200 201
      #   person.pets.count  # => 0
      #   person.pets.empty? # => true

      ##
      # :method: any?
V
Vijay Dev 已提交
202
      # Returns true if the collection is not empty.
203 204 205 206 207 208 209 210 211 212 213 214
      #
      #   class Person < ActiveRecord::Base
      #     has_many :pets
      #   end
      #
      #   person.pets.count # => 0
      #   person.pets.any?  # => false
      #
      #   person.pets << Pet.new(name: 'Snoop')
      #   person.pets.count # => 0
      #   person.pets.any?  # => true
      #
V
Vijay Dev 已提交
215
      # You can also pass a block to define criteria. The behaviour
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
      # is the same, it returns true if the collection based on the
      # criteria is not empty.
      #
      #   person.pets
      #   # => [#<Pet name: "Snoop", group: "dogs">]
      #
      #   person.pets.any? do |pet|
      #     pet.group == 'cats'
      #   end
      #   # => false
      #
      #   person.pets.any? do |pet|
      #     pet.group == 'dogs'
      #   end
      #   # => true

      ##
      # :method: many?
V
Vijay Dev 已提交
234
      # Returns true if the collection has more than one record.
235 236 237 238 239 240 241 242 243 244 245 246 247
      # Equivalent to +collection.size > 1+.
      #
      #   class Person < ActiveRecord::Base
      #     has_many :pets
      #   end
      #
      #   person.pets.count #=> 1
      #   person.pets.many? #=> false
      #
      #   person.pets << Pet.new(name: 'Snoopy')
      #   person.pets.count #=> 2
      #   person.pets.many? #=> true
      #
V
Vijay Dev 已提交
248
      # You can also pass a block to define criteria. The
249
      # behaviour is the same, it returns true if the collection
V
Vijay Dev 已提交
250
      # based on the criteria has more than one record.
251 252 253
      #
      #   person.pets
      #   # => [
V
Vijay Dev 已提交
254 255
      #   #      #<Pet name: "Gorby", group: "cats">,
      #   #      #<Pet name: "Puff", group: "cats">,
256 257 258 259 260 261 262 263 264 265 266 267
      #   #      #<Pet name: "Snoop", group: "dogs">
      #   #    ]
      #
      #   person.pets.many? do |pet|
      #     pet.group == 'dogs'
      #   end
      #   # => false
      #
      #   person.pets.many? do |pet|
      #     pet.group == 'cats'
      #   end
      #   # => true
268 269 270 271 272 273 274 275 276 277 278 279 280

      ##
      # :method: include?
      # Returns true if the given object is present in the collection.
      #
      #   class Person < ActiveRecord::Base
      #     has_many :pets
      #   end
      #
      #   person.pets # => [#<Pet id: 20, name: "Snoop">]
      #
      #   person.pets.include?(Pet.find(20)) # => true
      #   person.pets.include?(Pet.find(21)) # => false
281 282
      delegate :select, :find, :first, :last,
               :build, :create, :create!,
283
               :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq,
284 285 286 287 288 289
               :sum, :count, :size, :length, :empty?,
               :any?, :many?, :include?,
               :to => :@association

      def initialize(association)
        @association = association
J
Jon Leighton 已提交
290 291
        super association.klass, association.klass.arel_table
        merge! association.scoped
292 293
      end

294 295
      alias_method :new, :build

296 297 298 299
      def proxy_association
        @association
      end

J
Jon Leighton 已提交
300 301 302 303 304 305 306 307 308 309 310 311
      # We don't want this object to be put on the scoping stack, because
      # that could create an infinite loop where we call an @association
      # method, which gets the current scope, which is this object, which
      # delegates to @association, and so on.
      def scoping
        @association.scoped.scoping { yield }
      end

      def spawn
        scoped
      end

312
      def scoped(options = nil)
313
        association = @association
314

J
Jon Leighton 已提交
315
        super.extending! do
316 317 318 319
          define_method(:proxy_association) { association }
        end
      end

J
Jon Leighton 已提交
320 321 322 323
      def ==(other)
        load_target == other
      end

324 325 326 327 328
      def to_ary
        load_target.dup
      end
      alias_method :to_a, :to_ary

329
      # Adds one or more +records+ to the collection by setting their foreign keys
330
      # to the association‘s primary key. Returns +self+, so several appends may be
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
      # chained together.
      #
      #   class Person < ActiveRecord::Base
      #     has_many :pets
      #   end
      #
      #   person.pets.size # => 0
      #   person.pets << Pet.new(name: 'Fancy-Fancy')
      #   person.pets << [Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')]
      #   person.pets.size # => 3
      #
      #   person.id # => 1
      #   person.pets
      #   # => [
      #   #      #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
      #   #      #<Pet id: 2, name: "Spook", person_id: 1>,
      #   #      #<Pet id: 3, name: "Choo-Choo", person_id: 1>
      #   #    ]
349
      def <<(*records)
350
        proxy_association.concat(records) && self
351 352 353
      end
      alias_method :push, :<<

354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
      # Removes every object from the collection. This does not destroy
      # the objects, it sets their foreign keys to +NULL+. Returns +self+
      # so methods can be chained.
      #
      #   class Person < ActiveRecord::Base
      #     has_many :pets
      #   end
      #
      #   person.pets       # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
      #   person.pets.clear # => []
      #   person.pets.size  # => 0
      #
      #   Pet.find(1) # => #<Pet id: 1, name: "Snoop", group: "dogs", person_id: nil>
      #
      # If they are associated with +dependent: :destroy+ option, it deletes
      # them directly from the database.
      #
      #   class Person < ActiveRecord::Base
      #     has_many :pets, dependent: :destroy
      #   end
      #
V
Vijay Dev 已提交
375
      #   person.pets       # => [#<Pet id: 2, name: "Gorby", group: "cats", person_id: 2>]
376 377 378 379
      #   person.pets.clear # => []
      #   person.pets.size  # => 0
      #
      #   Pet.find(2) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=2
380 381 382 383 384 385
      def clear
        delete_all
        self
      end

      def reload
386
        proxy_association.reload
387 388 389 390 391
        self
      end
    end
  end
end