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 delegates unknown methods to @target via # method_missing. # # 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(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope) def initialize(klass, association) #:nodoc: @association = association super klass, klass.arel_table self.default_scoped = true merge! association.scope(nullify: false) end def target @association.target end def load_target @association.load_target end # Returns +true+ if the association has been loaded, otherwise +false+. # # person.pets.loaded? # => false # person.pets # person.pets.loaded? # => true def loaded? @association.loaded? end # Works in two ways. # # *First:* Specify a subset of fields to be selected from the result set. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.select(:name) # # => [ # # #, # # #, # # # # # ] # # person.pets.select([:id, :name]) # # => [ # # #, # # #, # # # # # ] # # Be careful because this also means you're initializing a model # object with only the fields that you've selected. If you attempt # to access a field that is not in the initialized record you'll # receive: # # person.pets.select(:name).first.person_id # # => ActiveModel::MissingAttributeError: missing attribute: person_id # # *Second:* You can pass a block so it can be used just like Array#select. # This builds an array of objects from the database for the scope, # converting them into an array and iterating through them using # Array#select. # # person.pets.select { |pet| pet.name =~ /oo/ } # # => [ # # #, # # # # # ] # # person.pets.select(:name) { |pet| pet.name =~ /oo/ } # # => [ # # #, # # # # # ] def select(select = nil, &block) @association.select(select, &block) end # 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 # # => [ # # #, # # #, # # # # # ] # # person.pets.find(1) # => # # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4 # # person.pets.find(2) { |pet| pet.name.downcase! } # # => # # # person.pets.find(2, 3) # # => [ # # #, # # # # # ] def find(*args, &block) @association.find(*args, &block) end # 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) # => [] def first(*args) @association.first(*args) end # 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) # => [] def last(*args) @association.last(*args) end # Returns a new object of the collection type that has been instantiated # with +attributes+ and linked to this object, but have not yet been saved. # You can pass an array of attributes hashes, this will return an array # with the new objects. # # class Person # has_many :pets # end # # person.pets.build # # => # # # person.pets.build(name: 'Fancy-Fancy') # # => # # # person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}]) # # => [ # # #, # # #, # # # # # ] # # person.pets.size # => 5 # size of the collection # person.pets.count # => 0 # count from database def build(attributes = {}, &block) @association.build(attributes, &block) end alias_method :new, :build # Returns a new object of the collection type that has been instantiated with # attributes, linked to this object and that has already been saved (if it # passes the validations). # # class Person # has_many :pets # end # # person.pets.create(name: 'Fancy-Fancy') # # => # # # person.pets.create([{name: 'Spook'}, {name: 'Choo-Choo'}]) # # => [ # # #, # # # # # ] # # person.pets.size # => 3 # person.pets.count # => 3 # # person.pets.find(1, 2, 3) # # => [ # # #, # # #, # # # # # ] def create(attributes = {}, &block) @association.create(attributes, &block) end # Like +create+, except that if the record is invalid, raises an exception. # # class Person # has_many :pets # end # # class Pet # validates :name, presence: true # end # # person.pets.create!(name: nil) # # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank def create!(attributes = {}, &block) @association.create!(attributes, &block) end # 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 def concat(*records) @association.concat(*records) end # Replaces 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 def replace(other_array) @association.replace(other_array) end # Deletes all the records from the collection. For +has_many+ associations, # the deletion is done according to the strategy specified by the :dependent # option. Returns an array with the deleted records. # # If no :dependent option is given, then it will follow the # default strategy. The default strategy is :nullify. This # sets the foreign keys to NULL. For, +has_many+ :through, # the default strategy is +delete_all+. # # class Person < ActiveRecord::Base # has_many :pets # dependent: :nullify option by default # end # # person.pets.size # => 3 # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.delete_all # # => [ # # #, # # #, # # # # # ] # # person.pets.size # => 0 # person.pets # => [] # # Pet.find(1, 2, 3) # # => [ # # #, # # #, # # # # # ] # # If it is set to :destroy all the objects from the collection # are removed by calling their +destroy+ method. See +destroy+ for more # information. # # class Person < ActiveRecord::Base # has_many :pets, dependent: :destroy # end # # person.pets.size # => 3 # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.delete_all # # => [ # # #, # # #, # # # # # ] # # Pet.find(1, 2, 3) # # => ActiveRecord::RecordNotFound # # If it is set to :delete_all, all the objects are deleted # *without* calling their +destroy+ method. # # class Person < ActiveRecord::Base # has_many :pets, dependent: :delete_all # end # # person.pets.size # => 3 # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.delete_all # # => [ # # #, # # #, # # # # # ] # # Pet.find(1, 2, 3) # # => ActiveRecord::RecordNotFound def delete_all @association.delete_all end # Deletes the records of the collection directly from the database. # This will _always_ remove the records ignoring the +:dependent+ # option. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets.size # => 3 # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.destroy_all # # person.pets.size # => 0 # person.pets # => [] # # Pet.find(1) # => Couldn't find Pet with id=1 def destroy_all @association.destroy_all end # Deletes the +records+ supplied and removes them from the collection. For # +has_many+ associations, the deletion is done according to the strategy # specified by the :dependent option. Returns an array with the # deleted records. # # If no :dependent option is given, then it will follow the default # strategy. The default strategy is :nullify. This sets the foreign # keys to NULL. For, +has_many+ :through, the default # strategy is +delete_all+. # # class Person < ActiveRecord::Base # has_many :pets # dependent: :nullify option by default # end # # person.pets.size # => 3 # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.delete(Pet.find(1)) # # => [#] # # person.pets.size # => 2 # person.pets # # => [ # # #, # # # # # ] # # Pet.find(1) # # => # # # If it is set to :destroy all the +records+ are removed by calling # their +destroy+ method. See +destroy+ for more information. # # class Person < ActiveRecord::Base # has_many :pets, dependent: :destroy # end # # person.pets.size # => 3 # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.delete(Pet.find(1), Pet.find(3)) # # => [ # # #, # # # # # ] # # person.pets.size # => 1 # person.pets # # => [#] # # Pet.find(1, 3) # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3) # # If it is set to :delete_all, all the +records+ are deleted # *without* calling their +destroy+ method. # # class Person < ActiveRecord::Base # has_many :pets, dependent: :delete_all # end # # person.pets.size # => 3 # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.delete(Pet.find(1)) # # => [#] # # person.pets.size # => 2 # person.pets # # => [ # # #, # # # # # ] # # Pet.find(1) # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1 # # You can pass +Fixnum+ or +String+ values, it finds the records # responding to the +id+ and executes delete on them. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets.size # => 3 # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.delete("1") # # => [#] # # person.pets.delete(2, 3) # # => [ # # #, # # # # # ] def delete(*records) @association.delete(*records) end # Destroys the +records+ supplied and removes them from the collection. # This method will _always_ remove record from the database ignoring # the +:dependent+ option. Returns an array with the removed records. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets.size # => 3 # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.destroy(Pet.find(1)) # # => [#] # # person.pets.size # => 2 # person.pets # # => [ # # #, # # # # # ] # # person.pets.destroy(Pet.find(2), Pet.find(3)) # # => [ # # #, # # # # # ] # # person.pets.size # => 0 # person.pets # => [] # # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3) # # You can pass +Fixnum+ or +String+ values, it finds the records # responding to the +id+ and then deletes them from the database. # # person.pets.size # => 3 # person.pets # # => [ # # #, # # #, # # # # # ] # # person.pets.destroy("4") # # => # # # person.pets.size # => 2 # person.pets # # => [ # # #, # # # # # ] # # person.pets.destroy(5, 6) # # => [ # # #, # # # # # ] # # person.pets.size # => 0 # person.pets # => [] # # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6) def destroy(*records) @association.destroy(*records) end # Specifies whether the records should be unique or not. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets.select(:name) # # => [ # # #, # # # # # ] # # person.pets.select(:name).distinct # # => [#] def distinct @association.distinct end alias uniq distinct # Count all records using SQL. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets.count # => 3 # person.pets # # => [ # # #, # # #, # # # # # ] def count(column_name = nil, options = {}) @association.count(column_name, options) end # Returns the size of the collection. If the collection hasn't been loaded, # it executes a SELECT COUNT(*) query. Else it calls collection.size. # # If the collection has been already loaded +size+ and +length+ are # equivalent. If not and you are going to need the records anyway # +length+ will take one less query. Otherwise +size+ is more efficient. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets.size # => 3 # # executes something like SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" = 1 # # person.pets # This will execute a SELECT * FROM query # # => [ # # #, # # #, # # # # # ] # # person.pets.size # => 3 # # Because the collection is already loaded, this will behave like # # collection.size and no SQL count query is executed. def size @association.size end # Returns the size of the collection calling +size+ on the target. # If the collection has been already loaded, +length+ and +size+ are # equivalent. If not and you are going to need the records anyway this # method will take one less query. Otherwise +size+ is more efficient. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets.length # => 3 # # executes something like SELECT "pets".* FROM "pets" WHERE "pets"."person_id" = 1 # # # Because the collection is loaded, you can # # call the collection with no additional queries: # person.pets # # => [ # # #, # # #, # # # # # ] def length @association.length end # Returns +true+ if the collection is empty. If the collection has been # loaded or the :counter_sql option is provided, it is equivalent # to collection.size.zero?. If the collection has not been loaded, # it is equivalent to collection.exists?. If the collection has # not already been loaded and you are going to fetch the records anyway it # is better to check collection.length.zero?. # # 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 def empty? @association.empty? end # 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 behavior # 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 def any?(&block) @association.any?(&block) end # 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 # behavior 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 def many?(&block) @association.many?(&block) end # 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 def include?(record) @association.include?(record) end 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.scope.scoping { yield } end # Returns a Relation object for the records in this association def scope @association.scope.tap do |scope| scope.proxy_association = @association end end # :nodoc: alias spawn scope # Equivalent to Array#==. Returns +true+ if the two arrays # contain the same number of elements and if each element is equal # to the corresponding element in the other array, otherwise returns # +false+. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets # # => [ # # #, # # # # # ] # # other = person.pets.to_ary # # person.pets == other # # => true # # other = [Pet.new(id: 1), Pet.new(id: 2)] # # person.pets == other # # => false def ==(other) load_target == other end # Returns a new array of objects from the collection. If the collection # hasn't been loaded, it fetches the records from the database. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets # # => [ # # #, # # #, # # # # # ] # # other_pets = person.pets.to_ary # # => [ # # #, # # #, # # # # # ] # # other_pets.replace([Pet.new(name: 'BooGoo')]) # # other_pets # # => [#] # # person.pets # # This is not affected by replace # # => [ # # #, # # #, # # # # # ] 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, :<< alias_method :append, :<< def prepend(*args) raise NoMethodError, "prepend on association is not defined. Please use << or append" end # Equivalent to +delete_all+. The difference is that returns +self+, instead # of an array with the deleted objects, so methods can be chained. See # +delete_all+ for more information. def clear delete_all self end # Reloads the collection from the database. Returns +self+. # Equivalent to collection(true). # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets # fetches pets from the database # # => [#] # # person.pets # uses the pets cache # # => [#] # # person.pets.reload # fetches pets from the database # # => [#] # # person.pets(true) # fetches pets from the database # # => [#] def reload proxy_association.reload self end end end end