diff options
Diffstat (limited to 'activerecord/lib/active_record/associations/collection_association.rb')
-rw-r--r-- | activerecord/lib/active_record/associations/collection_association.rb | 193 |
1 files changed, 89 insertions, 104 deletions
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 278c95e27b..ceedf150e3 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations # = Active Record Association Collection @@ -25,26 +27,13 @@ module ActiveRecord # +load_target+ and the +loaded+ flag are your friends. class CollectionAssociation < Association #:nodoc: # Implements the reader method, e.g. foo.items for Foo.has_many :items - def reader(force_reload = false) - if force_reload - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing an argument to force an association to reload is now - deprecated and will be removed in Rails 5.1. Please call `reload` - on the result collection proxy instead. - MSG - - klass.uncached { reload } - elsif stale_target? + def reader + if stale_target? reload end - if null_scope? - # Cache the proxy separately before the owner has an id - # or else a post-save proxy will still lack the id - @null_proxy ||= CollectionProxy.create(klass, self) - else - @proxy ||= CollectionProxy.create(klass, self) - end + @proxy ||= CollectionProxy.create(klass, self) + @proxy.reset_scope end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -55,51 +44,52 @@ module ActiveRecord # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items def ids_reader if loaded? - load_target.map do |record| - record.send(reflection.association_primary_key) - end + target.pluck(reflection.association_primary_key) else - @association_ids ||= ( - column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - scope.pluck(column) - ) + @association_ids ||= scope.pluck(reflection.association_primary_key) end end # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) - pk_type = reflection.primary_key_type + pk_type = reflection.association_primary_key_type ids = Array(ids).reject(&:blank?) ids.map! { |i| pk_type.cast(i) } - records = klass.where(reflection.association_primary_key => ids).index_by do |r| - r.send(reflection.association_primary_key) - end.values_at(*ids) - replace(records) + + primary_key = reflection.association_primary_key + records = klass.where(primary_key => ids).index_by do |r| + r.public_send(primary_key) + end.values_at(*ids).compact + + if records.size != ids.size + found_ids = records.map { |record| record.public_send(primary_key) } + not_found_ids = ids - found_ids + klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, primary_key, not_found_ids) + else + replace(records) + end end def reset super @target = [] + @association_ids = nil end 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 + 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 - scope.find(*args) + result end + else + scope.find(*args) end end @@ -192,11 +182,8 @@ module ActiveRecord # +delete_records+. They are in any case removed from the collection. def delete(*records) return if records.empty? - _options = records.extract_options! - dependent = _options[:dependent] || options[:dependent] - records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } - delete_or_destroy(records, dependent) + delete_or_destroy(records, options[:dependent]) end # Deletes the +records+ and removes them from this association calling @@ -222,11 +209,7 @@ module ActiveRecord # +count_records+, which is a method descendants have to provide. def size if !find_target? || loaded? - if association_scope.distinct_value - target.uniq.size - else - target.size - end + target.size elsif !association_scope.group_values.empty? load_target.size elsif !association_scope.distinct_value && target.is_a?(Array) @@ -253,13 +236,6 @@ module ActiveRecord end end - def distinct - seen = {} - load_target.find_all do |record| - seen[record.id] = true unless seen.key?(record.id) - end - end - # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. def replace(other_array) @@ -306,26 +282,9 @@ module ActiveRecord replace_on_target(record, index, skip_callbacks, &block) end - def replace_on_target(record, index, skip_callbacks) - callback(:before_add, record) unless skip_callbacks - - yield(record) if block_given? - - if index - @target[index] = record - else - append_record(record) - end - - callback(:after_add, record) unless skip_callbacks - set_inverse_instance(record) - - record - end - - def scope(opts = {}) - scope = super() - scope.none! if opts.fetch(:nullify, true) && null_scope? + def scope + scope = super + scope.none! if null_scope? scope end @@ -342,18 +301,17 @@ module ActiveRecord private def find_target - return scope.to_a if skip_statement_cache? + scope = self.scope + return scope.to_a if skip_statement_cache?(scope) conn = klass.connection - sc = reflection.association_scope_cache(conn, owner) do - StatementCache.create(conn) { |params| - as = AssociationScope.create { params.bind } - target_scope.merge as.scope(self, conn) - } + sc = reflection.association_scope_cache(conn, owner) do |params| + as = AssociationScope.create { params.bind } + target_scope.merge!(as.scope(self)) end binds = AssociationScope.get_bind_values(owner, reflection.chain) - sc.execute(binds, klass, conn) do |record| + sc.execute(binds, conn) do |record| set_inverse_instance(record) end end @@ -375,7 +333,7 @@ module ActiveRecord persisted.map! do |record| if mem_record = memory.delete(record) - ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name| + ((record.attribute_names & mem_record.attribute_names) - mem_record.changed_attribute_names_to_save).each do |name| mem_record[name] = record[name] end @@ -399,19 +357,22 @@ module ActiveRecord transaction do add_to_target(build_record(attributes)) do |record| yield(record) if block_given? - insert_record(record, true, raise) + insert_record(record, true, raise) { + @_was_loaded = loaded? + @association_ids = nil + } end end end end # Do the relevant stuff to insert the given record into the association collection. - def insert_record(record, validate = true, raise = false) - raise NotImplementedError - end - - def create_scope - scope.scope_for_create.stringify_keys + def insert_record(record, validate = true, raise = false, &block) + if raise + record.save!(validate: validate, &block) + else + record.save(validate: validate, &block) + end end def delete_or_destroy(records, method) @@ -435,8 +396,9 @@ module ActiveRecord records.each { |record| callback(:after_remove, record) } end - # Delete the given records from the association, using one of the methods :destroy, - # :delete_all or :nullify (or nil, in which case a default is used). + # Delete the given records from the association, + # using one of the methods +:destroy+, +:delete_all+ + # or +:nullify+ (or +nil+, in which case a default is used). def delete_records(records, method) raise NotImplementedError end @@ -461,19 +423,46 @@ module ActiveRecord end end - def concat_records(records, should_raise = false) + def concat_records(records, raise = false) result = true records.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? + add_to_target(record) do + unless owner.new_record? + result &&= insert_record(record, true, raise) { + @_was_loaded = loaded? + @association_ids = nil + } + end end end result && records end + def replace_on_target(record, index, skip_callbacks) + callback(:before_add, record) unless skip_callbacks + + set_inverse_instance(record) + + @_was_loaded = true + + yield(record) if block_given? + + if index + target[index] = record + elsif @_was_loaded || !loaded? + target << record + end + + callback(:after_add, record) unless skip_callbacks + + record + ensure + @_was_loaded = nil + end + def callback(method, record) callbacks_for(method).each do |callback| callback.call(method, owner, record) @@ -511,10 +500,6 @@ module ActiveRecord load_target.select { |r| ids.include?(r.id.to_s) } end end - - def append_record(record) - @target << record unless @target.include?(record) - end end end end |