# frozen_string_literal: true
module ActiveRecord
module Associations
# = Active Record Has Many Association
# This is the proxy that handles a has many association.
#
# If the association has a :through option further specialization
# is provided by its child HasManyThroughAssociation.
class HasManyAssociation < CollectionAssociation #:nodoc:
include ForeignAssociation
def handle_dependency
case options[:dependent]
when :restrict_with_exception
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
when :restrict_with_error
unless empty?
record = owner.class.human_attribute_name(reflection.name).downcase
owner.errors.add(:base, :'restrict_dependent_destroy.has_many', record: record)
throw(:abort)
end
when :destroy
# No point in executing the counter update since we're going to destroy the parent anyway
load_target.each { |t| t.destroyed_by_association = reflection }
destroy_all
when :destroy_async
load_target.each do |t|
t.destroyed_by_association = reflection
end
unless target.empty?
association_class = target.first.class
primary_key_column = association_class.primary_key.to_sym
ids = target.collect do |assoc|
assoc.public_send(primary_key_column)
end
enqueue_destroy_association(
owner_model_name: owner.class.to_s,
owner_id: owner.id,
association_class: reflection.klass.to_s,
association_ids: ids,
association_primary_key_column: primary_key_column,
ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
)
end
else
delete_all
end
end
def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
super
end
private
# Returns the number of records in this collection.
#
# If the association has a counter cache it gets that value. Otherwise
# it will attempt to do a count via SQL, bounded to :limit if
# there's one. Some configuration options like :group make it impossible
# to do an SQL count, in those cases the array count will be used.
#
# That does not depend on whether the collection has already been loaded
# or not. The +size+ method is the one that takes the loaded flag into
# account and delegates to +count_records+ if needed.
#
# If the collection is empty the target is set to an empty array and
# the loaded flag is set to true as well.
def count_records
count = if reflection.has_cached_counter?
owner.read_attribute(reflection.counter_cache_column).to_i
else
scope.count(:all)
end
# If there's nothing in the database and @target has no new records
# we are certain the current target is an empty array. This is a
# documented side-effect of the method that may avoid an extra SELECT.
loaded! if count == 0
[association_scope.limit_value, count].compact.min
end
def update_counter(difference, reflection = reflection())
if reflection.has_cached_counter?
owner.increment!(reflection.counter_cache_column, difference)
end
end
def update_counter_in_memory(difference, reflection = reflection())
if reflection.counter_must_be_updated_by_has_many?
counter = reflection.counter_cache_column
owner.increment(counter, difference)
owner.send(:"clear_#{counter}_change")
end
end
def delete_count(method, scope)
if method == :delete_all
scope.delete_all
else
scope.update_all(nullified_owner_attributes)
end
end
def delete_or_nullify_all_records(method)
count = delete_count(method, scope)
update_counter(-count)
count
end
# Deletes the records according to the :dependent option.
def delete_records(records, method)
if method == :destroy
records.each(&:destroy!)
update_counter(-records.length) unless reflection.inverse_updates_counter_cache?
else
scope = self.scope.where(reflection.klass.primary_key => records)
update_counter(-delete_count(method, scope))
end
end
def concat_records(records, *)
update_counter_if_success(super, records.length)
end
def _create_record(attributes, *)
if attributes.is_a?(Array)
super
else
update_counter_if_success(super, 1)
end
end
def update_counter_if_success(saved_successfully, difference)
if saved_successfully
update_counter_in_memory(difference)
end
saved_successfully
end
def difference(a, b)
a - b
end
def intersection(a, b)
a & b
end
end
end
end