module ActiveRecord module Associations # Association proxies in Active Record are middlemen between the object that # holds the association, known as the @owner, and the actual associated # object, known as the @target. The kind of association any proxy is # about is available in @reflection. That's an instance of the class # ActiveRecord::Reflection::AssociationReflection. # # For example, given # # class Blog < ActiveRecord::Base # has_many :posts # end # # blog = Blog.first # # the association proxy in blog.posts has the object in +blog+ as # @owner, the collection of its posts as @target, and # the @reflection object represents a :has_many macro. # # This class has most of the basic instance methods removed, and delegates # unknown methods to @target via method_missing. As a # corner case, it even removes the +class+ method and that's why you get # # blog.posts.class # => Array # # though the object behind blog.posts is not an Array, but an # ActiveRecord::Associations::HasManyAssociation. # # The @target 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. class CollectionProxy < Relation delegate :target, :load_target, :loaded?, :to => :@association ## # :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 # # => [ # # #, # # #, # # # # # ] # # person.pets.first # => # # # person.pets.first(2) # # => [ # # #, # # # # # ] # # another_person_without.pets # => [] # another_person_without.pets.first # => nil # another_person_without.pets.first(3) # => [] ## # :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 # # => [ # # #, # # #, # # # # # ] # # person.pets.last # => # # # person.pets.last(2) # # => [ # # #, # # # # # ] # # another_person_without.pets # => [] # another_person_without.pets.last # => nil # another_person_without.pets.last(3) # => [] ## # :method: concat # 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. # # class Person < ActiveRecord::Base # pets :has_many # end # # 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 # # person.id # => 1 # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')]) # person.pets.size # => 5 ## # :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 # # => [#] # # other_pets = [Pet.new(name: 'Puff', group: 'celebrities'] # # person.pets.replace(other_pets) # # person.pets # # => [#] # # If the supplied array has an incorrect association type, it raises # an ActiveRecord::AssociationTypeMismatch error: # # 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 # => [] ## # :method: empty? # Returns true if the collection is empty. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets.count # => 1 # person.pets.empty? # => false # # person.pets.delete_all # # person.pets.count # => 0 # person.pets.empty? # => true ## # :method: any? # Returns true if the collection is not empty. # # 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 # # You can also pass a block to define criteria. The behaviour # is the same, it returns true if the collection based on the # criteria is not empty. # # person.pets # # => [#] # # person.pets.any? do |pet| # pet.group == 'cats' # end # # => false # # person.pets.any? do |pet| # pet.group == 'dogs' # end # # => true ## # :method: many? # Returns true if the collection has more than one record. # 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 # # You can also pass a block to define criteria. The # behaviour is the same, it returns true if the collection # based on the criteria has more than one record. # # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.many? do |pet| # pet.group == 'dogs' # end # # => false # # person.pets.many? do |pet| # pet.group == 'cats' # end # # => true ## # :method: include? # Returns true if the given object is present in the collection. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets # => [#] # # person.pets.include?(Pet.find(20)) # => true # person.pets.include?(Pet.find(21)) # => false delegate :select, :find, :first, :last, :build, :create, :create!, :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq, :sum, :count, :size, :length, :empty?, :any?, :many?, :include?, :to => :@association def initialize(association) @association = association super association.klass, association.klass.arel_table merge! association.scoped end alias_method :new, :build def proxy_association @association end # 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 def scoped(options = nil) association = @association super.extending! do define_method(:proxy_association) { association } end end def ==(other) load_target == other end def to_ary load_target.dup end alias_method :to_a, :to_ary # Adds one or more +records+ to the collection by setting their foreign keys # to the association‘s primary key. Returns +self+, so several appends may be # 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 # # => [ # # #, # # #, # # # # # ] def <<(*records) proxy_association.concat(records) && self end alias_method :push, :<< # 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 # => [#] # person.pets.clear # => [] # person.pets.size # => 0 # # Pet.find(1) # => # # # 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 # # person.pets # => [#] # person.pets.clear # => [] # person.pets.size # => 0 # # Pet.find(2) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=2 def clear delete_all self end def reload proxy_association.reload self end end end end