Created
June 10, 2016 20:58
-
-
Save bf4/315a08632cfbd94c9fd33762e90457d5 to your computer and use it in GitHub Desktop.
Rails 4.2.6 patch for autosave and accepts_nested_attributes_for from issuing N+1 queries on each eager loaded association
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# We noticed that adding `accepts_nested_attributes_for :comments, allow_destroy: true` | |
# caused extra queries to be run when serializing comments in a post | |
# and including the post name in each comment resource object (this is JSON API, names changed): | |
# | |
# blog_id = params.permit(:id)[:id] | |
# Blog = Blog.where(id: blog_id).includes(posts: [:comments]).first | |
# comments = blog.posts.flat_map(&:comments) | |
# comment_fields = {comments: [:id, :title, :body, :post], posts: [:name]} | |
# comment_includes = [:post] | |
# | |
# This was strange, so we found in ActiveRecord::NestedAttributes::ClassMethods | |
# | |
# https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/nested_attributes.rb#L309 | |
# # Note that the <tt>:autosave</tt> option is automatically enabled on every | |
# # association that accepts_nested_attributes_for is used for. | |
# def accepts_nested_attributes_for(*attr_names) | |
# #.... | |
# attr_names.each do |association_name| | |
# if reflection = _reflect_on_association(association_name) | |
# reflection.autosave = true | |
# | |
# i.e. it was setting `autosave: true` on the reflection. | |
# | |
# It turns out that `ActiveRecord::Reflection::MacroReflection.autosave=` | |
# is setting an instance variable `@automatic_inverse_of = false` which | |
# disables Rails from automatically adding `inverse_of` as if we had declared `has_many :comments, inverse_of: :post`, | |
# Post._reflect_on_association(:comments).send(:automatic_inverse_of) #=> :post | |
# which meant that when we found the comments for the post and then asked for the post, | |
# it was issuing a NEW QUERY FOR EACH resource object. | |
# | |
# In the PR to Rails that introduced automatic inverse_of look up https://github.com/rails/rails/pull/9522 | |
# Sean Griffin comments: https://github.com/rails/rails/commit/26d19b4661f3d89a075b5f05d926c578ff0c730f#commitcomment-9482625 | |
# > people don't expect accepts_nested_attributes_for to break automatic inverses. | |
# and then | |
# > Turns out having autosave and inverse_of can cause a record to be saved more than once, | |
# > because it's still marked as changed? inside of after_save, after_update, and after_create. | |
# > And fixing that turned out to be an insurmountable task right now | |
# > (assuming it's a breaking change we'd like to make which I'm not sure we do). | |
# | |
# And right now there's an open PR to resolve this issue that is applied below: | |
# Patch from https://github.com/rails/rails/pull/23197 at ref: 2577c78e0a19ac0af2b54e6316791f32c787c62c | |
ActiveSupport.on_load(:active_record) do | |
ActiveRecord::Reflection::MacroReflection.class_eval do | |
def autosave=(autosave) | |
# - @automatic_inverse_of = false | |
@options[:autosave] = autosave | |
_, parent_reflection = self.parent_reflection | |
# changed in master to: | |
# parent_reflection = self.parent_reflection | |
if parent_reflection | |
parent_reflection.autosave = autosave | |
end | |
end | |
end | |
ActiveRecord::AutosaveAssociation.class_eval do | |
# Returns true if this record has triggered saving of other records through | |
# autosave associations, and will remain true for the length of the cycle | |
# until all autosaves have completed. | |
# | |
# This is necessary so that saves triggered on associated records know not | |
# to re-save the parent if the autosave is bidirectional. | |
def _triggering_record? # :nodoc: | |
defined?(@_triggering_record) ? @_triggering_record : false | |
end | |
# Temporarily mark this record as a triggering_record for the duration of | |
# the block call | |
def with_self_as_triggering_record | |
@_triggering_record = true | |
yield | |
ensure | |
@_triggering_record = false | |
end | |
private | |
# Saves any new associated records, or all loaded autosave associations if | |
# <tt>:autosave</tt> is enabled on the association. | |
# | |
# In addition, it destroys all children that were marked for destruction | |
# with #mark_for_destruction. | |
# | |
# This all happens inside a transaction, _if_ the Transactions module is included into | |
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default. | |
def save_collection_association(reflection) | |
if association = association_instance_get(reflection.name) | |
autosave = reflection.options[:autosave] | |
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) | |
if autosave | |
records_to_destroy = records.select(&:marked_for_destruction?) | |
records_to_destroy.each { |record| association.destroy(record) } | |
records -= records_to_destroy | |
end | |
records.each do |record| | |
# - next if record.destroyed? | |
next if record.destroyed? || record._triggering_record? | |
saved = true | |
# + added wrapping method: with_self_as_triggering_record | |
with_self_as_triggering_record do | |
if autosave != false && (@new_record_before_save || record.new_record?) | |
if autosave | |
saved = association.insert_record(record, false) | |
else | |
association.insert_record(record) unless reflection.nested? | |
end | |
elsif autosave | |
saved = record.save(validate: false) | |
end | |
end | |
raise ActiveRecord::Rollback unless saved | |
end | |
end | |
# reconstruct the scope now that we know the owner's id | |
association.reset_scope if association.respond_to?(:reset_scope) | |
end | |
end | |
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled | |
# on the association. | |
# | |
# In addition, it will destroy the association if it was marked for | |
# destruction with #mark_for_destruction. | |
# | |
# This all happens inside a transaction, _if_ the Transactions module is included into | |
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default. | |
def save_has_one_association(reflection) | |
association = association_instance_get(reflection.name) | |
record = association && association.load_target | |
# - if record && !record.destroyed? | |
if record && !record.destroyed? && !record._triggering_record? | |
autosave = reflection.options[:autosave] | |
if autosave && record.marked_for_destruction? | |
record.destroy | |
elsif autosave != false | |
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id | |
if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key) | |
unless reflection.through_reflection | |
record[reflection.foreign_key] = key | |
end | |
# + added wrapping method: with_self_as_triggering_record | |
saved = with_self_as_triggering_record do | |
record.save(validate: !autosave) | |
end | |
raise ActiveRecord::Rollback if !saved && autosave | |
saved | |
end | |
end | |
end | |
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled. | |
# | |
# In addition, it will destroy the association if it was marked for destruction. | |
def save_belongs_to_association(reflection) | |
association = association_instance_get(reflection.name) | |
record = association && association.load_target | |
# - if record && !record.destroyed? | |
if record && !record.destroyed? && !record._triggering_record? | |
autosave = reflection.options[:autosave] | |
if autosave && record.marked_for_destruction? | |
self[reflection.foreign_key] = nil | |
record.destroy | |
elsif autosave != false | |
# - saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) | |
# + BEGIN | |
if record.new_record? || (autosave && record.changed_for_autosave?) | |
saved = with_self_as_triggering_record do | |
record.save(validate: !autosave) | |
end | |
end | |
# + END | |
if association.updated? | |
association_id = record.send(reflection.options[:primary_key] || :id) | |
self[reflection.foreign_key] = association_id | |
association.loaded! | |
end | |
saved if autosave | |
end | |
end | |
end | |
end | |
end | |
end | |
# These files haven't changed much between Rails 4.2.x and master | |
# git diff 4-2-stable..2577c78 activerecord/lib/active_record/autosave_association.rb activerecord/lib/active_record/reflection.rb | pbcopy | |
# ActiveRecord::AutosaveAssociation | |
# diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb | |
# index b7402d1..ba8c6a7 100644 | |
# --- a/activerecord/lib/active_record/autosave_association.rb | |
# +++ b/activerecord/lib/active_record/autosave_association.rb | |
# @@ -1,10 +1,10 @@ | |
# module ActiveRecord | |
# # = Active Record Autosave Association | |
# # | |
# - # +AutosaveAssociation+ is a module that takes care of automatically saving | |
# + # AutosaveAssociation is a module that takes care of automatically saving | |
# # associated records when their parent is saved. In addition to saving, it | |
# # also destroys any associated records that were marked for destruction. | |
# - # (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>). | |
# + # (See #mark_for_destruction and #marked_for_destruction?). | |
# # | |
# # Saving of the parent, its associations, and the destruction of marked | |
# # associations, all happen inside a transaction. This should never leave the | |
# @@ -22,7 +22,7 @@ module ActiveRecord | |
# # | |
# # == Validation | |
# # | |
# - # Children records are validated unless <tt>:validate</tt> is +false+. | |
# + # Child records are validated unless <tt>:validate</tt> is +false+. | |
# # | |
# # == Callbacks | |
# # | |
# @@ -125,7 +125,6 @@ module ActiveRecord | |
# # Now it _is_ removed from the database: | |
# # | |
# # Comment.find_by(id: id).nil? # => true | |
# - | |
# module AutosaveAssociation | |
# extend ActiveSupport::Concern | |
# | |
# @@ -141,9 +140,11 @@ module ActiveRecord | |
# | |
# included do | |
# Associations::Builder::Association.extensions << AssociationBuilderExtension | |
# + mattr_accessor :index_nested_attribute_errors, instance_writer: false | |
# + self.index_nested_attribute_errors = false | |
# end | |
# | |
# - module ClassMethods | |
# + module ClassMethods # :nodoc: | |
# private | |
# | |
# def define_non_cyclic_method(name, &block) | |
# @@ -198,7 +199,7 @@ module ActiveRecord | |
# after_create save_method | |
# after_update save_method | |
# else | |
# - define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) } | |
# + define_non_cyclic_method(save_method) { throw(:abort) if save_belongs_to_association(reflection) == false } | |
# before_save save_method | |
# end | |
# | |
# @@ -214,8 +215,15 @@ module ActiveRecord | |
# method = :validate_single_association | |
# end | |
# | |
# - define_non_cyclic_method(validation_method) { send(method, reflection) } | |
# + define_non_cyclic_method(validation_method) do | |
# + send(method, reflection) | |
# + # TODO: remove the following line as soon as the return value of | |
# + # callbacks is ignored, that is, returning `false` does not | |
# + # display a deprecation warning or halts the callback chain. | |
# + true | |
# + end | |
# validate validation_method | |
# + after_validation :_ensure_no_duplicate_errors | |
# end | |
# end | |
# end | |
# @@ -227,7 +235,7 @@ module ActiveRecord | |
# super | |
# end | |
# | |
# - # Marks this record to be destroyed as part of the parents save transaction. | |
# + # Marks this record to be destroyed as part of the parent's save transaction. | |
# # This does _not_ actually destroy the record instantly, rather child record will be destroyed | |
# # when <tt>parent.save</tt> is called. | |
# # | |
# @@ -236,7 +244,7 @@ module ActiveRecord | |
# @marked_for_destruction = true | |
# end | |
# | |
# - # Returns whether or not this record will be destroyed as part of the parents save transaction. | |
# + # Returns whether or not this record will be destroyed as part of the parent's save transaction. | |
# # | |
# # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model. | |
# def marked_for_destruction? | |
# @@ -262,6 +270,16 @@ module ActiveRecord | |
# new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave? | |
# end | |
# | |
# + # Returns true if this record has triggered saving of other records through | |
# + # autosave associations, and will remain true for the length of the cycle | |
# + # until all autosaves have completed. | |
# + # | |
# + # This is necessary so that saves triggered on associated records know not | |
# + # to re-save the parent if the autosave is bidirectional. | |
# + def _triggering_record? # :nodoc: | |
# + defined?(@_triggering_record) ? @_triggering_record : false | |
# + end | |
# + | |
# private | |
# | |
# # Returns the record for an association collection that should be validated | |
# @@ -271,9 +289,9 @@ module ActiveRecord | |
# if new_record | |
# association && association.target | |
# elsif autosave | |
# - association.target.find_all { |record| record.changed_for_autosave? } | |
# + association.target.find_all(&:changed_for_autosave?) | |
# else | |
# - association.target.find_all { |record| record.new_record? } | |
# + association.target.find_all(&:new_record?) | |
# end | |
# end | |
# | |
# @@ -309,7 +327,7 @@ module ActiveRecord | |
# def validate_collection_association(reflection) | |
# if association = association_instance_get(reflection.name) | |
# if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) | |
# - records.each { |record| association_valid?(reflection, record) } | |
# + records.each_with_index { |record, index| association_valid?(reflection, record, index) } | |
# end | |
# end | |
# end | |
# @@ -317,17 +335,36 @@ module ActiveRecord | |
# # Returns whether or not the association is valid and applies any errors to | |
# # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> | |
# # enabled records if they're marked_for_destruction? or destroyed. | |
# - def association_valid?(reflection, record) | |
# + def association_valid?(reflection, record, index=nil) | |
# return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) | |
# | |
# validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) | |
# unless valid = record.valid?(validation_context) | |
# if reflection.options[:autosave] | |
# + indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors) | |
# + | |
# record.errors.each do |attribute, message| | |
# - attribute = "#{reflection.name}.#{attribute}" | |
# + if indexed_attribute | |
# + attribute = "#{reflection.name}[#{index}].#{attribute}" | |
# + else | |
# + attribute = "#{reflection.name}.#{attribute}" | |
# + end | |
# errors[attribute] << message | |
# errors[attribute].uniq! | |
# end | |
# + | |
# + record.errors.details.each_key do |attribute| | |
# + if indexed_attribute | |
# + reflection_attribute = "#{reflection.name}[#{index}].#{attribute}" | |
# + else | |
# + reflection_attribute = "#{reflection.name}.#{attribute}" | |
# + end | |
# + | |
# + record.errors.details[attribute].each do |error| | |
# + errors.details[reflection_attribute] << error | |
# + errors.details[reflection_attribute].uniq! | |
# + end | |
# + end | |
# else | |
# errors.add(reflection.name) | |
# end | |
# @@ -342,11 +379,20 @@ module ActiveRecord | |
# true | |
# end | |
# | |
# + # Temporarily mark this record as a triggering_record for the duration of | |
# + # the block call | |
# + def with_self_as_triggering_record | |
# + @_triggering_record = true | |
# + yield | |
# + ensure | |
# + @_triggering_record = false | |
# + end | |
# + | |
# # Saves any new associated records, or all loaded autosave associations if | |
# # <tt>:autosave</tt> is enabled on the association. | |
# # | |
# # In addition, it destroys all children that were marked for destruction | |
# - # with mark_for_destruction. | |
# + # with #mark_for_destruction. | |
# # | |
# # This all happens inside a transaction, _if_ the Transactions module is included into | |
# # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. | |
# @@ -362,18 +408,20 @@ module ActiveRecord | |
# end | |
# | |
# records.each do |record| | |
# - next if record.destroyed? | |
# + next if record.destroyed? || record._triggering_record? | |
# | |
# saved = true | |
# | |
# - if autosave != false && (@new_record_before_save || record.new_record?) | |
# - if autosave | |
# - saved = association.insert_record(record, false) | |
# - else | |
# - association.insert_record(record) unless reflection.nested? | |
# + with_self_as_triggering_record do | |
# + if autosave != false && (@new_record_before_save || record.new_record?) | |
# + if autosave | |
# + saved = association.insert_record(record, false) | |
# + else | |
# + association.insert_record(record) unless reflection.nested? | |
# + end | |
# + elsif autosave | |
# + saved = record.save(validate: false) | |
# end | |
# - elsif autosave | |
# - saved = record.save(:validate => false) | |
# end | |
# | |
# raise ActiveRecord::Rollback unless saved | |
# @@ -389,7 +437,7 @@ module ActiveRecord | |
# # on the association. | |
# # | |
# # In addition, it will destroy the association if it was marked for | |
# - # destruction with mark_for_destruction. | |
# + # destruction with #mark_for_destruction. | |
# # | |
# # This all happens inside a transaction, _if_ the Transactions module is included into | |
# # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. | |
# @@ -397,7 +445,7 @@ module ActiveRecord | |
# association = association_instance_get(reflection.name) | |
# record = association && association.load_target | |
# | |
# - if record && !record.destroyed? | |
# + if record && !record.destroyed? && !record._triggering_record? | |
# autosave = reflection.options[:autosave] | |
# | |
# if autosave && record.marked_for_destruction? | |
# @@ -410,7 +458,10 @@ module ActiveRecord | |
# record[reflection.foreign_key] = key | |
# end | |
# | |
# - saved = record.save(:validate => !autosave) | |
# + saved = with_self_as_triggering_record do | |
# + record.save(validate: !autosave) | |
# + end | |
# + | |
# raise ActiveRecord::Rollback if !saved && autosave | |
# saved | |
# end | |
# @@ -431,14 +482,18 @@ module ActiveRecord | |
# def save_belongs_to_association(reflection) | |
# association = association_instance_get(reflection.name) | |
# record = association && association.load_target | |
# - if record && !record.destroyed? | |
# + if record && !record.destroyed? && !record._triggering_record? | |
# autosave = reflection.options[:autosave] | |
# | |
# if autosave && record.marked_for_destruction? | |
# self[reflection.foreign_key] = nil | |
# record.destroy | |
# elsif autosave != false | |
# - saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) | |
# + if record.new_record? || (autosave && record.changed_for_autosave?) | |
# + saved = with_self_as_triggering_record do | |
# + record.save(validate: !autosave) | |
# + end | |
# + end | |
# | |
# if association.updated? | |
# association_id = record.send(reflection.options[:primary_key] || :id) | |
# @@ -450,5 +505,11 @@ module ActiveRecord | |
# end | |
# end | |
# end | |
# + | |
# + def _ensure_no_duplicate_errors | |
# + errors.messages.each_key do |attribute| | |
# + errors[attribute].uniq! | |
# + end | |
# + end | |
# end | |
# end | |
# ActiveRecord::Reflection::MacroReflection | |
# diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb | |
# index b46df26..e86b030 100644 | |
# --- a/activerecord/lib/active_record/reflection.rb | |
# +++ b/activerecord/lib/active_record/reflection.rb | |
# @@ -40,9 +40,9 @@ module ActiveRecord | |
# ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection) | |
# end | |
# | |
# - # \Reflection enables interrogating of Active Record classes and objects | |
# - # about their associations and aggregations. This information can, | |
# - # for example, be used in a form builder that takes an Active Record object | |
# + # \Reflection enables the ability to examine the associations and aggregations of | |
# + # Active Record classes and objects. This information, for example, | |
# + # can be used in a form builder that takes an Active Record object | |
# # and creates input fields for all of the attributes depending on their type | |
# # and displays the associations to other objects. | |
# # | |
# @@ -62,20 +62,20 @@ module ActiveRecord | |
# aggregate_reflections[aggregation.to_s] | |
# end | |
# | |
# - # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value. | |
# + # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value. | |
# # | |
# # Account.reflections # => {"balance" => AggregateReflection} | |
# # | |
# - # @api public | |
# def reflections | |
# @__reflections ||= begin | |
# ref = {} | |
# | |
# _reflections.each do |name, reflection| | |
# - parent_name, parent_reflection = reflection.parent_reflection | |
# + parent_reflection = reflection.parent_reflection | |
# | |
# - if parent_name | |
# - ref[parent_name] = parent_reflection | |
# + if parent_reflection | |
# + parent_name = parent_reflection.name | |
# + ref[parent_name.to_s] = parent_reflection | |
# else | |
# ref[name] = reflection | |
# end | |
# @@ -95,10 +95,10 @@ module ActiveRecord | |
# # Account.reflect_on_all_associations # returns an array of all associations | |
# # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations | |
# # | |
# - # @api public | |
# def reflect_on_all_associations(macro = nil) | |
# association_reflections = reflections.values | |
# - macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections | |
# + association_reflections.select! { |reflection| reflection.macro == macro } if macro | |
# + association_reflections | |
# end | |
# | |
# # Returns the AssociationReflection object for the +association+ (use the symbol). | |
# @@ -106,31 +106,42 @@ module ActiveRecord | |
# # Account.reflect_on_association(:owner) # returns the owner AssociationReflection | |
# # Invoice.reflect_on_association(:line_items).macro # returns :has_many | |
# # | |
# - # @api public | |
# def reflect_on_association(association) | |
# reflections[association.to_s] | |
# end | |
# | |
# - # @api private | |
# def _reflect_on_association(association) #:nodoc: | |
# _reflections[association.to_s] | |
# end | |
# | |
# # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. | |
# - # | |
# - # @api public | |
# def reflect_on_all_autosave_associations | |
# reflections.values.select { |reflection| reflection.options[:autosave] } | |
# end | |
# | |
# - def clear_reflections_cache #:nodoc: | |
# + def clear_reflections_cache # :nodoc: | |
# @__reflections = nil | |
# end | |
# end | |
# | |
# - # Holds all the methods that are shared between MacroReflection, AssociationReflection | |
# - # and ThroughReflection | |
# + # Holds all the methods that are shared between MacroReflection and ThroughReflection. | |
# + # | |
# + # AbstractReflection | |
# + # MacroReflection | |
# + # AggregateReflection | |
# + # AssociationReflection | |
# + # HasManyReflection | |
# + # HasOneReflection | |
# + # BelongsToReflection | |
# + # HasAndBelongsToManyReflection | |
# + # ThroughReflection | |
# + # PolymorphicReflection | |
# + # RuntimeReflection | |
# class AbstractReflection # :nodoc: | |
# + def through_reflection? | |
# + false | |
# + end | |
# + | |
# def table_name | |
# klass.table_name | |
# end | |
# @@ -159,17 +170,24 @@ module ActiveRecord | |
# | |
# JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: | |
# | |
# - def join_keys(assoc_klass) | |
# + def join_keys(association_klass) | |
# JoinKeys.new(foreign_key, active_record_primary_key) | |
# end | |
# | |
# - def source_macro | |
# - ActiveSupport::Deprecation.warn(<<-MSG.squish) | |
# - ActiveRecord::Base.source_macro is deprecated and will be removed | |
# - without replacement. | |
# - MSG | |
# + def constraints | |
# + scope_chain.flatten | |
# + end | |
# | |
# - macro | |
# + def counter_cache_column | |
# + if belongs_to? | |
# + if options[:counter_cache] == true | |
# + "#{active_record.name.demodulize.underscore.pluralize}_count" | |
# + elsif options[:counter_cache] | |
# + options[:counter_cache].to_s | |
# + end | |
# + else | |
# + options[:counter_cache] ? options[:counter_cache].to_s : "#{name}_count" | |
# + end | |
# end | |
# | |
# def inverse_of | |
# @@ -185,17 +203,54 @@ module ActiveRecord | |
# end | |
# end | |
# end | |
# + | |
# + # This shit is nasty. We need to avoid the following situation: | |
# + # | |
# + # * An associated record is deleted via record.destroy | |
# + # * Hence the callbacks run, and they find a belongs_to on the record with a | |
# + # :counter_cache options which points back at our owner. So they update the | |
# + # counter cache. | |
# + # * In which case, we must make sure to *not* update the counter cache, or else | |
# + # it will be decremented twice. | |
# + # | |
# + # Hence this method. | |
# + def inverse_which_updates_counter_cache | |
# + return @inverse_which_updates_counter_cache if defined?(@inverse_which_updates_counter_cache) | |
# + @inverse_which_updates_counter_cache = klass.reflect_on_all_associations(:belongs_to).find do |inverse| | |
# + inverse.counter_cache_column == counter_cache_column | |
# + end | |
# + end | |
# + alias inverse_updates_counter_cache? inverse_which_updates_counter_cache | |
# + | |
# + def inverse_updates_counter_in_memory? | |
# + inverse_of && inverse_which_updates_counter_cache == inverse_of | |
# + end | |
# + | |
# + # Returns whether a counter cache should be used for this association. | |
# + # | |
# + # The counter_cache option must be given on either the owner or inverse | |
# + # association, and the column must be present on the owner. | |
# + def has_cached_counter? | |
# + options[:counter_cache] || | |
# + inverse_which_updates_counter_cache && inverse_which_updates_counter_cache.options[:counter_cache] && | |
# + !!active_record.columns_hash[counter_cache_column] | |
# + end | |
# + | |
# + def counter_must_be_updated_by_has_many? | |
# + !inverse_updates_counter_in_memory? && has_cached_counter? | |
# + end | |
# + | |
# + def alias_candidate(name) | |
# + "#{plural_name}_#{name}" | |
# + end | |
# + | |
# + def chain | |
# + collect_join_chain | |
# + end | |
# end | |
# + | |
# # Base class for AggregateReflection and AssociationReflection. Objects of | |
# # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. | |
# - # | |
# - # MacroReflection | |
# - # AssociationReflection | |
# - # AggregateReflection | |
# - # HasManyReflection | |
# - # HasOneReflection | |
# - # BelongsToReflection | |
# - # ThroughReflection | |
# class MacroReflection < AbstractReflection | |
# # Returns the name of the macro. | |
# # | |
# @@ -226,9 +281,8 @@ module ActiveRecord | |
# end | |
# | |
# def autosave=(autosave) | |
# - @automatic_inverse_of = false | |
# @options[:autosave] = autosave | |
# - _, parent_reflection = self.parent_reflection | |
# + parent_reflection = self.parent_reflection | |
# if parent_reflection | |
# parent_reflection.autosave = autosave | |
# end | |
# @@ -296,7 +350,7 @@ module ActiveRecord | |
# end | |
# | |
# attr_reader :type, :foreign_type | |
# - attr_accessor :parent_reflection # [:name, Reflection] | |
# + attr_accessor :parent_reflection # Reflection | |
# | |
# def initialize(name, scope, options, active_record) | |
# super | |
# @@ -343,14 +397,6 @@ module ActiveRecord | |
# @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) | |
# end | |
# | |
# - def counter_cache_column | |
# - if options[:counter_cache] == true | |
# - "#{active_record.name.demodulize.underscore.pluralize}_count" | |
# - elsif options[:counter_cache] | |
# - options[:counter_cache].to_s | |
# - end | |
# - end | |
# - | |
# def check_validity! | |
# check_validity_of_inverse! | |
# end | |
# @@ -359,13 +405,10 @@ module ActiveRecord | |
# return unless scope | |
# | |
# if scope.arity > 0 | |
# - ActiveSupport::Deprecation.warn(<<-MSG.squish) | |
# + raise ArgumentError, <<-MSG.squish | |
# The association scope '#{name}' is instance dependent (the scope | |
# - block takes an argument). Preloading happens before the individual | |
# - instances are created. This means that there is no instance being | |
# - passed to the association scope. This will most likely result in | |
# - broken or incorrect behavior. Joining, Preloading and eager loading | |
# - of these associations is deprecated and will be removed in the future. | |
# + block takes an argument). Preloading instance dependent scopes is | |
# + not supported. | |
# MSG | |
# end | |
# end | |
# @@ -385,10 +428,16 @@ module ActiveRecord | |
# | |
# # A chain of reflections from this one back to the owner. For more see the explanation in | |
# # ThroughReflection. | |
# - def chain | |
# + def collect_join_chain | |
# [self] | |
# end | |
# | |
# + # This is for clearing cache on the reflection. Useful for tests that need to compare | |
# + # SQL queries on associations. | |
# + def clear_association_scope_cache # :nodoc: | |
# + @association_scope_cache.clear | |
# + end | |
# + | |
# def nested? | |
# false | |
# end | |
# @@ -399,6 +448,10 @@ module ActiveRecord | |
# scope ? [[scope]] : [[]] | |
# end | |
# | |
# + def has_scope? | |
# + scope | |
# + end | |
# + | |
# def has_inverse? | |
# inverse_name | |
# end | |
# @@ -444,28 +497,7 @@ module ActiveRecord | |
# # Returns +true+ if +self+ is a +has_one+ reflection. | |
# def has_one?; false; end | |
# | |
# - def association_class | |
# - case macro | |
# - when :belongs_to | |
# - if polymorphic? | |
# - Associations::BelongsToPolymorphicAssociation | |
# - else | |
# - Associations::BelongsToAssociation | |
# - end | |
# - when :has_many | |
# - if options[:through] | |
# - Associations::HasManyThroughAssociation | |
# - else | |
# - Associations::HasManyAssociation | |
# - end | |
# - when :has_one | |
# - if options[:through] | |
# - Associations::HasOneThroughAssociation | |
# - else | |
# - Associations::HasOneAssociation | |
# - end | |
# - end | |
# - end | |
# + def association_class; raise NotImplementedError; end | |
# | |
# def polymorphic? | |
# options[:polymorphic] | |
# @@ -474,6 +506,18 @@ module ActiveRecord | |
# VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] | |
# INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key] | |
# | |
# + def add_as_source(seed) | |
# + seed | |
# + end | |
# + | |
# + def add_as_polymorphic_through(reflection, seed) | |
# + seed + [PolymorphicReflection.new(self, reflection)] | |
# + end | |
# + | |
# + def add_as_through(seed) | |
# + seed + [self] | |
# + end | |
# + | |
# protected | |
# | |
# def actual_source_reflection # FIXME: this is a horrible name | |
# @@ -483,14 +527,7 @@ module ActiveRecord | |
# private | |
# | |
# def calculate_constructable(macro, options) | |
# - case macro | |
# - when :belongs_to | |
# - !polymorphic? | |
# - when :has_one | |
# - !options[:through] | |
# - else | |
# - true | |
# - end | |
# + true | |
# end | |
# | |
# # Attempts to find the inverse association name automatically. | |
# @@ -506,7 +543,7 @@ module ActiveRecord | |
# end | |
# end | |
# | |
# - # returns either nil or the inverse association name that it finds. | |
# + # returns either false or the inverse association name that it finds. | |
# def automatic_inverse_of | |
# if can_find_inverse_of_automatically?(self) | |
# inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym | |
# @@ -583,42 +620,66 @@ module ActiveRecord | |
# end | |
# | |
# class HasManyReflection < AssociationReflection # :nodoc: | |
# - def initialize(name, scope, options, active_record) | |
# - super(name, scope, options, active_record) | |
# - end | |
# - | |
# def macro; :has_many; end | |
# | |
# def collection?; true; end | |
# - end | |
# | |
# - class HasOneReflection < AssociationReflection # :nodoc: | |
# - def initialize(name, scope, options, active_record) | |
# - super(name, scope, options, active_record) | |
# + def association_class | |
# + if options[:through] | |
# + Associations::HasManyThroughAssociation | |
# + else | |
# + Associations::HasManyAssociation | |
# + end | |
# end | |
# + end | |
# | |
# + class HasOneReflection < AssociationReflection # :nodoc: | |
# def macro; :has_one; end | |
# | |
# def has_one?; true; end | |
# - end | |
# | |
# - class BelongsToReflection < AssociationReflection # :nodoc: | |
# - def initialize(name, scope, options, active_record) | |
# - super(name, scope, options, active_record) | |
# + def association_class | |
# + if options[:through] | |
# + Associations::HasOneThroughAssociation | |
# + else | |
# + Associations::HasOneAssociation | |
# + end | |
# end | |
# | |
# + private | |
# + | |
# + def calculate_constructable(macro, options) | |
# + !options[:through] | |
# + end | |
# + end | |
# + | |
# + class BelongsToReflection < AssociationReflection # :nodoc: | |
# def macro; :belongs_to; end | |
# | |
# def belongs_to?; true; end | |
# | |
# - def join_keys(assoc_klass) | |
# - key = polymorphic? ? association_primary_key(assoc_klass) : association_primary_key | |
# + def association_class | |
# + if polymorphic? | |
# + Associations::BelongsToPolymorphicAssociation | |
# + else | |
# + Associations::BelongsToAssociation | |
# + end | |
# + end | |
# + | |
# + def join_keys(association_klass) | |
# + key = polymorphic? ? association_primary_key(association_klass) : association_primary_key | |
# JoinKeys.new(key, foreign_key) | |
# end | |
# | |
# def join_id_for(owner) # :nodoc: | |
# owner[foreign_key] | |
# end | |
# + | |
# + private | |
# + | |
# + def calculate_constructable(macro, options) | |
# + !polymorphic? | |
# + end | |
# end | |
# | |
# class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: | |
# @@ -646,6 +707,10 @@ module ActiveRecord | |
# @source_reflection_name = delegate_reflection.options[:source] | |
# end | |
# | |
# + def through_reflection? | |
# + true | |
# + end | |
# + | |
# def klass | |
# @klass ||= delegate_reflection.compute_class(class_name) | |
# end | |
# @@ -704,14 +769,16 @@ module ActiveRecord | |
# # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>, | |
# # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>] | |
# # | |
# - def chain | |
# - @chain ||= begin | |
# - a = source_reflection.chain | |
# - b = through_reflection.chain | |
# - chain = a + b | |
# - chain[0] = self # Use self so we don't lose the information from :source_type | |
# - chain | |
# - end | |
# + def collect_join_chain | |
# + collect_join_reflections [self] | |
# + end | |
# + | |
# + # This is for clearing cache on the reflection. Useful for tests that need to compare | |
# + # SQL queries on associations. | |
# + def clear_association_scope_cache # :nodoc: | |
# + delegate_reflection.clear_association_scope_cache | |
# + source_reflection.clear_association_scope_cache | |
# + through_reflection.clear_association_scope_cache | |
# end | |
# | |
# # Consider the following example: | |
# @@ -755,23 +822,19 @@ module ActiveRecord | |
# end | |
# end | |
# | |
# - def join_keys(assoc_klass) | |
# - source_reflection.join_keys(assoc_klass) | |
# + def has_scope? | |
# + scope || options[:source_type] || | |
# + source_reflection.has_scope? || | |
# + through_reflection.has_scope? | |
# end | |
# | |
# - # The macro used by the source association | |
# - def source_macro | |
# - ActiveSupport::Deprecation.warn(<<-MSG.squish) | |
# - ActiveRecord::Base.source_macro is deprecated and will be removed | |
# - without replacement. | |
# - MSG | |
# - | |
# - source_reflection.source_macro | |
# + def join_keys(association_klass) | |
# + source_reflection.join_keys(association_klass) | |
# end | |
# | |
# # A through association is nested if there would be more than one join table | |
# def nested? | |
# - chain.length > 2 | |
# + source_reflection.through_reflection? || through_reflection.through_reflection? | |
# end | |
# | |
# # We want to use the klass from this reflection, rather than just delegate straight to | |
# @@ -801,7 +864,7 @@ module ActiveRecord | |
# def source_reflection_name # :nodoc: | |
# return @source_reflection_name if @source_reflection_name | |
# | |
# - names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq | |
# + names = [name.to_s.singularize, name].collect(&:to_sym).uniq | |
# names = names.find_all { |n| | |
# through_reflection.klass._reflect_on_association(n) | |
# } | |
# @@ -865,6 +928,33 @@ module ActiveRecord | |
# check_validity_of_inverse! | |
# end | |
# | |
# + def constraints | |
# + scope_chain = source_reflection.constraints | |
# + scope_chain << scope if scope | |
# + scope_chain | |
# + end | |
# + | |
# + def add_as_source(seed) | |
# + collect_join_reflections seed | |
# + end | |
# + | |
# + def add_as_polymorphic_through(reflection, seed) | |
# + collect_join_reflections(seed + [PolymorphicReflection.new(self, reflection)]) | |
# + end | |
# + | |
# + def add_as_through(seed) | |
# + collect_join_reflections(seed + [self]) | |
# + end | |
# + | |
# + def collect_join_reflections(seed) | |
# + a = source_reflection.add_as_source seed | |
# + if options[:source_type] | |
# + through_reflection.add_as_polymorphic_through self, a | |
# + else | |
# + through_reflection.add_as_through a | |
# + end | |
# + end | |
# + | |
# protected | |
# | |
# def actual_source_reflection # FIXME: this is a horrible name | |
# @@ -889,5 +979,81 @@ module ActiveRecord | |
# delegate(*delegate_methods, to: :delegate_reflection) | |
# | |
# end | |
# + | |
# + class PolymorphicReflection < ThroughReflection # :nodoc: | |
# + def initialize(reflection, previous_reflection) | |
# + @reflection = reflection | |
# + @previous_reflection = previous_reflection | |
# + end | |
# + | |
# + def klass | |
# + @reflection.klass | |
# + end | |
# + | |
# + def scope | |
# + @reflection.scope | |
# + end | |
# + | |
# + def table_name | |
# + @reflection.table_name | |
# + end | |
# + | |
# + def plural_name | |
# + @reflection.plural_name | |
# + end | |
# + | |
# + def join_keys(association_klass) | |
# + @reflection.join_keys(association_klass) | |
# + end | |
# + | |
# + def type | |
# + @reflection.type | |
# + end | |
# + | |
# + def constraints | |
# + @reflection.constraints + [source_type_info] | |
# + end | |
# + | |
# + def source_type_info | |
# + type = @previous_reflection.foreign_type | |
# + source_type = @previous_reflection.options[:source_type] | |
# + lambda { |object| where(type => source_type) } | |
# + end | |
# + end | |
# + | |
# + class RuntimeReflection < PolymorphicReflection # :nodoc: | |
# + attr_accessor :next | |
# + | |
# + def initialize(reflection, association) | |
# + @reflection = reflection | |
# + @association = association | |
# + end | |
# + | |
# + def klass | |
# + @association.klass | |
# + end | |
# + | |
# + def table_name | |
# + klass.table_name | |
# + end | |
# + | |
# + def constraints | |
# + @reflection.constraints | |
# + end | |
# + | |
# + def source_type_info | |
# + @reflection.source_type_info | |
# + end | |
# + | |
# + def alias_candidate(name) | |
# + "#{plural_name}_#{name}_join" | |
# + end | |
# + | |
# + def alias_name | |
# + Arel::Table.new(table_name) | |
# + end | |
# + | |
# + def all_includes; yield; end | |
# + end | |
# end | |
# end | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
see rails/rails#23197 (and then rails/rails@26d19b4#commitcomment-9482625 in rails/rails#9522 )