class ActiveRecord::Associations::CollectionAssociation
:nodoc:load_target and the loaded flag are your friends.
If you need to work on all current children, new and existing records,
collection because new records may have been added to the target, etc.
If you look directly to the database you cannot assume that’s the entire
non-empty and still lack children waiting to be read from the database.
ones created with build are added to the target. So, the target may be
does not fetch records from the database until it needs them, but new
You need to be careful with assumptions regarding the target: The proxy
+:through association+ option.
defined by has_and_belongs_to_many, has_many or has_many with
CollectionAssociation class provides common methods to the collections
HasManyThroughAssociation + ThroughAssociation => has_many :through
HasManyAssociation => has_many
CollectionAssociation:
collections. See the class hierarchy in Association.
ease the implementation of association proxies that represent
CollectionAssociation is an abstract class that provides common stuff to
= Active Record Association Collection
def add_to_target(record, skip_callbacks = false)
def add_to_target(record, skip_callbacks = false) callback(:before_add, record) unless skip_callbacks yield(record) if block_given? if association_scope.distinct_value && index = @target.index(record) @target[index] = record else @target << record end callback(:after_add, record) unless skip_callbacks set_inverse_instance(record) record end
def any?
Returns true if the collections is not empty.
def any? if block_given? load_target.any? { |*block_args| yield(*block_args) } else !empty? end end
def build(attributes = {}, &block)
def build(attributes = {}, &block) if attributes.is_a?(Array) attributes.collect { |attr| build(attr, &block) } else add_to_target(build_record(attributes)) do |record| yield(record) if block_given? end end end
def callback(method, record)
def callback(method, record) callbacks_for(method).each do |callback| callback.call(method, owner, record) end end
def callbacks_for(callback_name)
def callbacks_for(callback_name) full_callback_name = "#{callback_name}_for_#{reflection.name}" owner.class.send(full_callback_name) end
def concat(*records)
be chained. Since << flattens its argument list and inserts each record,
Add +records+ to this association. Returns +self+ so method calls may
def concat(*records) load_target if owner.new_record? if owner.new_record? concat_records(records) else transaction { concat_records(records) } end end
def concat_records(records, should_raise = false)
def concat_records(records, should_raise = false) result = true records.flatten.each do |record| raise_on_type_mismatch!(record) add_to_target(record) do |rec| result &&= insert_record(rec, true, should_raise) unless owner.new_record? end end result && records end
def count(column_name = nil, count_options = {})
Count all records using SQL. Construct options and pass them with
def count(column_name = nil, count_options = {}) # TODO: Remove count_options argument as soon we remove support to # activerecord-deprecated_finders. column_name, count_options = nil, column_name if column_name.is_a?(Hash) relation = scope if association_scope.distinct_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. column_name ||= reflection.klass.primary_key relation = relation.distinct end value = relation.count(column_name) limit = options[:limit] offset = options[:offset] if limit || offset [ [value - offset.to_i, 0].max, limit.to_i ].min else value end end
def create(attributes = {}, &block)
def create(attributes = {}, &block) create_record(attributes, &block) end
def create!(attributes = {}, &block)
def create!(attributes = {}, &block) create_record(attributes, true, &block) end
def create_record(attributes, raise = false, &block)
def create_record(attributes, raise = false, &block) unless owner.persisted? raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" end if attributes.is_a?(Array) attributes.collect { |attr| create_record(attr, raise, &block) } else transaction do add_to_target(build_record(attributes)) do |record| yield(record) if block_given? insert_record(record, true, raise) end end end end
def create_scope
def create_scope scope.scope_for_create.stringify_keys end
def delete(*records)
are actually removed from the database, that depends precisely on
provided by descendants. Note this method does not imply the records
This method is abstract in the sense that +delete_records+ has to be
+after_remove+ callbacks.
Removes +records+ from this association calling +before_remove+ and
def delete(*records) _options = records.extract_options! dependent = _options[:dependent] || options[:dependent] if records.first == :all if loaded? || dependent == :destroy delete_or_destroy(load_target, dependent) else delete_records(:all, dependent) end else records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } delete_or_destroy(records, dependent) end end
def delete_all(dependent = nil)
@author.books.delete_all(:delete_all)
@author.books.delete_all(:nullify)
Example:
You can force a particular deletion strategy by passing a parameter.
deletion strategy for the association is applied.
if the `:dependent` value is `:destroy` then in that case the `:delete_all`
on the associated records. It honors the `:dependent` option. However
Removes all records from the association without calling callbacks
def delete_all(dependent = nil) if dependent.present? && ![:nullify, :delete_all].include?(dependent) raise ArgumentError, "Valid values are :nullify or :delete_all" end dependent = if dependent.present? dependent elsif options[:dependent] == :destroy :delete_all else options[:dependent] end delete(:all, dependent: dependent).tap do reset loaded! end end
def delete_or_destroy(records, method)
def delete_or_destroy(records, method) records = records.flatten records.each { |record| raise_on_type_mismatch!(record) } existing_records = records.reject { |r| r.new_record? } if existing_records.empty? remove_records(existing_records, records, method) else transaction { remove_records(existing_records, records, method) } end end
def delete_records(records, method)
Delete the given records from the association, using one of the methods :destroy,
def delete_records(records, method) raise NotImplementedError end
def destroy(*records)
Note that this method removes records from the database ignoring the
+before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
Deletes the +records+ and removes them from this association calling
def destroy(*records) records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } delete_or_destroy(records, :destroy) end
def destroy_all
Destroy all the records from this association.
def destroy_all destroy(load_target).tap do reset loaded! end end
def distinct
def distinct seen = {} load_target.find_all do |record| seen[record.id] = true unless seen.key?(record.id) end end
def empty?
loaded and you are going to fetch the records anyway it is better to
collection.exists?. If the collection has not already been
collection has not been loaded, it is equivalent to
it is equivalent to collection.size.zero?. If the
If the collection has been loaded
Returns true if the collection is empty.
def empty? if loaded? size.zero? else @target.blank? && !scope.exists? end end
def fetch_first_nth_or_last_using_find?(args)
* owner is new record
* target already loaded
Otherwise, go to the database only if none of the following are true:
If the args is just a non-empty options hash, go to the database.
the database, or by getting the target, and then taking the first/last item from that?
Should we deal with assoc.first or assoc.last by issuing an independent query to
def fetch_first_nth_or_last_using_find?(args) if args.first.is_a?(Hash) true else !(loaded? || owner.new_record? || target.any? { |record| record.new_record? || record.changed? }) end end
def fifth(*args)
def fifth(*args) first_nth_or_last(:fifth, *args) end
def find(*args)
def find(*args) if block_given? load_target.find(*args) { |*block_args| yield(*block_args) } else if options[:inverse_of] && loaded? args_flatten = args.flatten raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank? result = find_by_scan(*args) result_size = Array(result).size if !result || result_size != args_flatten.size scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size) else result end else scope.find(*args) end end end
def find_by_scan(*args)
If the :inverse_of option has been
def find_by_scan(*args) expects_array = args.first.kind_of?(Array) ids = args.flatten.compact.map{ |arg| arg.to_s }.uniq if ids.size == 1 id = ids.first record = load_target.detect { |r| id == r.id.to_s } expects_array ? [ record ] : record else load_target.select { |r| ids.include?(r.id.to_s) } end end
def find_target
def find_target records = scope.to_a records.each { |record| set_inverse_instance(record) } records end
def first(*args)
def first(*args) first_nth_or_last(:first, *args) end
def first_nth_or_last(type, *args)
def first_nth_or_last(type, *args) args.shift if args.first.is_a?(Hash) && args.first.empty? collection = fetch_first_nth_or_last_using_find?(args) ? scope : load_target collection.send(type, *args).tap do |record| set_inverse_instance record if record.is_a? ActiveRecord::Base end end
def forty_two(*args)
def forty_two(*args) first_nth_or_last(:forty_two, *args) end
def fourth(*args)
def fourth(*args) first_nth_or_last(:fourth, *args) end
def ids_reader
def ids_reader if loaded? load_target.map do |record| record.send(reflection.association_primary_key) end else column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" scope.pluck(column) end end
def ids_writer(ids)
def ids_writer(ids) pk_column = reflection.primary_key_column ids = Array(ids).reject { |id| id.blank? } ids.map! { |i| pk_column.type_cast(i) } replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids)) end
def include?(record)
def include?(record) if record.is_a?(reflection.klass) if record.new_record? include_in_memory?(record) else loaded? ? target.include?(record) : scope.exists?(record) end else false end end
def include_in_memory?(record)
def include_in_memory?(record) if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) assoc = owner.association(reflection.through_reflection.name) assoc.reader.any? { |source| target = source.send(reflection.source_reflection.name) target.respond_to?(:include?) ? target.include?(record) : target == record } || target.include?(record) else target.include?(record) end end
def insert_record(record, validate = true, raise = false)
def insert_record(record, validate = true, raise = false) raise NotImplementedError end
def last(*args)
def last(*args) first_nth_or_last(:last, *args) end
def length
equivalent. If not and you are going to need the records anyway this
If the collection has been already loaded +length+ and +size+ are
Returns the size of the collection calling +size+ on the target.
def length load_target.size end
def load_target
def load_target if find_target? @target = merge_target_lists(find_target, target) end loaded! target end
def many?
Returns true if the collection has more than 1 record.
def many? if block_given? load_target.many? { |*block_args| yield(*block_args) } else size > 1 end end
def merge_target_lists(persisted, memory)
* Any changes made to attributes on objects in the memory array are to be preserved
* The order of the persisted array is to be preserved
* The final array must not have duplicates
So the task of this method is to merge them according to the following rules:
and in the memory array.
in-memory (memory). The same record may be represented in the persisted array
We have some records loaded from the database (persisted) and some that are
def merge_target_lists(persisted, memory) return persisted if memory.empty? return memory if persisted.empty? persisted.map! do |record| if mem_record = memory.delete(record) ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name| mem_record[name] = record[name] end mem_record else record end end persisted + memory end
def null_scope?
def null_scope? owner.new_record? && !foreign_key_present? end
def reader(force_reload = false)
def reader(force_reload = false) if force_reload klass.uncached { reload } elsif stale_target? reload end @proxy ||= CollectionProxy.create(klass, self) end
def remove_records(existing_records, records, method)
def remove_records(existing_records, records, method) records.each { |record| callback(:before_remove, record) } delete_records(existing_records, method) if existing_records.any? records.each { |record| target.delete(record) } records.each { |record| callback(:after_remove, record) } end
def replace(other_array)
Replace this collection with +other_array+. This will perform a diff
def replace(other_array) other_array.each { |val| raise_on_type_mismatch!(val) } original_target = load_target.dup if owner.new_record? replace_records(other_array, original_target) else transaction { replace_records(other_array, original_target) } end end
def replace_records(new_target, original_target)
def replace_records(new_target, original_target) delete(target - new_target) unless concat(new_target - target) @target = original_target raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \ "new records could not be saved." end target end
def reset
def reset super @target = [] end
def scope(opts = {})
def scope(opts = {}) scope = super() scope.none! if opts.fetch(:nullify, true) && null_scope? scope end
def second(*args)
def second(*args) first_nth_or_last(:second, *args) end
def select(*fields)
def select(*fields) if block_given? load_target.select.each { |e| yield e } else scope.select(*fields) end end
def size
This method is abstract in the sense that it relies on
+length+ will take one less query. Otherwise +size+ is more efficient.
equivalent. If not and you are going to need the records anyway
If the collection has been already loaded +size+ and +length+ are
collection.size if it has.
query if the collection hasn't been loaded, and calling
Returns the size of the collection by executing a SELECT COUNT(*)
def size if !find_target? || loaded? if association_scope.distinct_value target.uniq.size else target.size end elsif !loaded? && !association_scope.group_values.empty? load_target.size elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array) unsaved_records = target.select { |r| r.new_record? } unsaved_records.size + count_records else count_records end end
def third(*args)
def third(*args) first_nth_or_last(:third, *args) end
def transaction(*args)
# same effect as calling Book.transaction
Author.first.books.transaction do
end
has_many :books
class Author < ActiveRecord::Base
Starts a transaction in the association class's database connection.
def transaction(*args) reflection.klass.transaction(*args) do yield end end
def writer(records)
def writer(records) replace(records) end