aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations')
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb3
-rw-r--r--activerecord/lib/active_record/associations/association.rb8
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb1
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb46
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb18
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb20
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb19
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb69
-rw-r--r--activerecord/lib/active_record/associations/foreign_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb81
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb31
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb21
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb87
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one.rb8
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb13
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb18
25 files changed, 250 insertions, 236 deletions
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 2b7e4f28c5..021bc32237 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -2,8 +2,7 @@ require 'active_support/core_ext/string/conversions'
module ActiveRecord
module Associations
- # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
- # ActiveRecord::Associations::ThroughAssociationScope
+ # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency
class AliasTracker # :nodoc:
attr_reader :aliases
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 7c729676a7..c7b396f3d4 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -251,6 +251,14 @@ module ActiveRecord
initialize_attributes(record)
end
end
+
+ # Returns true if statement cache should be skipped on the association reader.
+ def skip_statement_cache?
+ reflection.scope_chain.any?(&:any?) ||
+ scope.eager_loading? ||
+ klass.scope_attributes? ||
+ reflection.source_reflection.active_record.default_scopes.any?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 2416167834..48437a1c9e 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -147,6 +147,7 @@ module ActiveRecord
scope.includes! item.includes_values
end
+ scope.unscope!(*item.unscope_values)
scope.where_clause += item.where_clause
scope.order_values |= item.order_values
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 260a0c6a2d..41698c5360 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -10,7 +10,7 @@ module ActiveRecord
def replace(record)
if record
raise_on_type_mismatch!(record)
- update_counters(record)
+ update_counters_on_replace(record)
replace_keys(record)
set_inverse_instance(record)
@updated = true
@@ -32,45 +32,37 @@ module ActiveRecord
end
def decrement_counters # :nodoc:
- with_cache_name { |name| decrement_counter name }
+ update_counters(-1)
end
def increment_counters # :nodoc:
- with_cache_name { |name| increment_counter name }
+ update_counters(1)
end
private
- def find_target?
- !loaded? && foreign_key_present? && klass
- end
-
- def with_cache_name
- counter_cache_name = reflection.counter_cache_column
- return unless counter_cache_name && owner.persisted?
- yield counter_cache_name
+ def update_counters(by)
+ if require_counter_update? && foreign_key_present?
+ if target && !stale_target?
+ target.increment!(reflection.counter_cache_column, by)
+ else
+ klass.update_counters(target_id, reflection.counter_cache_column => by)
+ end
+ end
end
- def update_counters(record)
- with_cache_name do |name|
- return unless different_target? record
- record.class.increment_counter(name, record.id)
- decrement_counter name
- end
+ def find_target?
+ !loaded? && foreign_key_present? && klass
end
- def decrement_counter(counter_cache_name)
- if foreign_key_present?
- klass.decrement_counter(counter_cache_name, target_id)
- end
+ def require_counter_update?
+ reflection.counter_cache_column && owner.persisted?
end
- def increment_counter(counter_cache_name)
- if foreign_key_present?
- klass.increment_counter(counter_cache_name, target_id)
- if target && !stale_target?
- target.increment(counter_cache_name)
- end
+ def update_counters_on_replace(record)
+ if require_counter_update? && different_target?(record)
+ record.increment!(reflection.counter_cache_column)
+ decrement_counters
end
end
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index ba1b1814d1..d0534056d9 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -9,7 +9,7 @@
# - CollectionAssociation
# - HasManyAssociation
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class Association #:nodoc:
class << self
attr_accessor :extensions
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 97eb007f62..dae468ba54 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -1,4 +1,4 @@
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class BelongsTo < SingularAssociation #:nodoc:
def self.macro
:belongs_to
@@ -33,16 +33,24 @@ module ActiveRecord::Associations::Builder
if (@_after_create_counter_called ||= false)
@_after_create_counter_called = false
- elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable?
- model = reflection.klass
+ elsif attribute_changed?(foreign_key) && !new_record?
+ if reflection.polymorphic?
+ model = attribute(reflection.foreign_type).try(:constantize)
+ model_was = attribute_was(reflection.foreign_type).try(:constantize)
+ else
+ model = reflection.klass
+ model_was = reflection.klass
+ end
+
foreign_key_was = attribute_was foreign_key
foreign_key = attribute foreign_key
if foreign_key && model.respond_to?(:increment_counter)
model.increment_counter(cache_column, foreign_key)
end
- if foreign_key_was && model.respond_to?(:decrement_counter)
- model.decrement_counter(cache_column, foreign_key_was)
+
+ if foreign_key_was && model_was.respond_to?(:decrement_counter)
+ model_was.decrement_counter(cache_column, foreign_key_was)
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 2ff67f904d..56a8dc4e18 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -2,7 +2,7 @@
require 'active_record/associations'
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class CollectionAssociation < Association #:nodoc:
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index ffd9c9d6fc..b888148841 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -1,9 +1,9 @@
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class HasAndBelongsToMany # :nodoc:
- class JoinTableResolver
+ class JoinTableResolver # :nodoc:
KnownTable = Struct.new :join_table
- class KnownClass
+ class KnownClass # :nodoc:
def initialize(lhs_class, rhs_class_name)
@lhs_class = lhs_class
@rhs_class_name = rhs_class_name
@@ -46,7 +46,7 @@ module ActiveRecord::Associations::Builder
join_model = Class.new(ActiveRecord::Base) {
class << self;
- attr_accessor :class_resolver
+ attr_accessor :left_model
attr_accessor :name
attr_accessor :table_name_resolver
attr_accessor :left_reflection
@@ -58,25 +58,29 @@ module ActiveRecord::Associations::Builder
end
def self.compute_type(class_name)
- class_resolver.compute_type class_name
+ left_model.compute_type class_name
end
def self.add_left_association(name, options)
- belongs_to name, options
+ belongs_to name, required: false, **options
self.left_reflection = _reflect_on_association(name)
end
def self.add_right_association(name, options)
rhs_name = name.to_s.singularize.to_sym
- belongs_to rhs_name, options
+ belongs_to rhs_name, required: false, **options
self.right_reflection = _reflect_on_association(rhs_name)
end
+ def self.retrieve_connection
+ left_model.retrieve_connection
+ end
+
}
join_model.name = "HABTM_#{association_name.to_s.camelize}"
join_model.table_name_resolver = habtm
- join_model.class_resolver = lhs_model
+ join_model.left_model = lhs_model
join_model.add_left_association :left_side, anonymous_class: lhs_model
join_model.add_right_association association_name, belongs_to_options(options)
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 1c1b47bd56..7864d4c536 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -1,11 +1,11 @@
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class HasMany < CollectionAssociation #:nodoc:
def self.macro
:has_many
end
def self.valid_options(options)
- super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type]
+ super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors]
end
def self.valid_dependent_options
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index a272d3c781..9d64ae877b 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -1,4 +1,4 @@
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class HasOne < SingularAssociation #:nodoc:
def self.macro
:has_one
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 42542f188e..58a9c8ff24 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,6 +1,6 @@
# This class is inherited by the has_one and belongs_to association classes
-module ActiveRecord::Associations::Builder
+module ActiveRecord::Associations::Builder # :nodoc:
class SingularAssociation < Association #:nodoc:
def self.valid_options(options)
super + [:dependent, :primary_key, :inverse_of, :required]
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 6caadb4ce8..f32dddb8f0 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -28,6 +28,12 @@ module ActiveRecord
# 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?
reload
@@ -54,8 +60,10 @@ module ActiveRecord
record.send(reflection.association_primary_key)
end
else
- column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
- scope.pluck(column)
+ @association_ids ||= (
+ column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
+ scope.pluck(column)
+ )
end
end
@@ -432,12 +440,7 @@ module ActiveRecord
private
def get_records
- if reflection.scope_chain.any?(&:any?) ||
- scope.eager_loading? ||
- klass.scope_attributes?
-
- return scope.to_a
- end
+ return scope.to_a if skip_statement_cache?
conn = klass.connection
sc = reflection.association_scope_cache(conn, owner) do
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index ddeafb40ea..fe693cfbb6 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -112,7 +112,7 @@ module ActiveRecord
end
# Finds an object in the collection responding to the +id+. Uses the same
- # rules as <tt>ActiveRecord::Base.find</tt>. Returns <tt>ActiveRecord::RecordNotFound</tt>
+ # rules as ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound
# error if the object cannot be found.
#
# class Person < ActiveRecord::Base
@@ -127,7 +127,7 @@ module ActiveRecord
# # ]
#
# person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
- # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4
+ # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=4
#
# person.pets.find(2) { |pet| pet.name.downcase! }
# # => #<Pet id: 2, name: "fancy-fancy", person_id: 1>
@@ -171,27 +171,27 @@ module ActiveRecord
@association.first(*args)
end
- # Same as +first+ except returns only the second record.
+ # Same as #first except returns only the second record.
def second(*args)
@association.second(*args)
end
- # Same as +first+ except returns only the third record.
+ # Same as #first except returns only the third record.
def third(*args)
@association.third(*args)
end
- # Same as +first+ except returns only the fourth record.
+ # Same as #first except returns only the fourth record.
def fourth(*args)
@association.fourth(*args)
end
- # Same as +first+ except returns only the fifth record.
+ # Same as #first except returns only the fifth record.
def fifth(*args)
@association.fifth(*args)
end
- # Same as +first+ except returns only the forty second record.
+ # Same as #first except returns only the forty second record.
# Also known as accessing "the reddit".
def forty_two(*args)
@association.forty_two(*args)
@@ -227,6 +227,31 @@ module ActiveRecord
@association.last(*args)
end
+ # Gives a record (or N records if a parameter is supplied) from the collection
+ # using the same rules as <tt>ActiveRecord::Base.take</tt>.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>,
+ # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
+ # # ]
+ #
+ # person.pets.take # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
+ #
+ # person.pets.take(2)
+ # # => [
+ # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
+ # # #<Pet id: 2, name: "Spook", person_id: 1>
+ # # ]
+ #
+ # another_person_without.pets # => []
+ # another_person_without.pets.take # => nil
+ # another_person_without.pets.take(2) # => []
def take(n = nil)
@association.take(n)
end
@@ -290,7 +315,7 @@ module ActiveRecord
@association.create(attributes, &block)
end
- # Like +create+, except that if the record is invalid, raises an exception.
+ # Like #create, except that if the record is invalid, raises an exception.
#
# class Person
# has_many :pets
@@ -307,8 +332,8 @@ module ActiveRecord
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+
+ # 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
@@ -364,7 +389,7 @@ module ActiveRecord
# specified by the +:dependent+ option. If no +:dependent+ option is given,
# then it will follow the default strategy.
#
- # For +has_many :through+ associations, the default deletion strategy is
+ # For <tt>has_many :through</tt> associations, the default deletion strategy is
# +:delete_all+.
#
# For +has_many+ associations, the default deletion strategy is +:nullify+.
@@ -399,7 +424,7 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: nil>
# # ]
#
- # Both +has_many+ and +has_many :through+ dependencies default to the
+ # Both +has_many+ and <tt>has_many :through</tt> dependencies default to the
# +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+.
# Records are not instantiated and callbacks will not be fired.
#
@@ -418,7 +443,7 @@ module ActiveRecord
# person.pets.delete_all
#
# Pet.find(1, 2, 3)
- # # => ActiveRecord::RecordNotFound
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
#
# If it is set to <tt>:delete_all</tt>, all the objects are deleted
# *without* calling their +destroy+ method.
@@ -438,7 +463,7 @@ module ActiveRecord
# person.pets.delete_all
#
# Pet.find(1, 2, 3)
- # # => ActiveRecord::RecordNotFound
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
def delete_all(dependent = nil)
@association.delete_all(dependent)
end
@@ -475,7 +500,7 @@ module ActiveRecord
# then it will follow the default strategy. Returns an array with the
# deleted records.
#
- # For +has_many :through+ associations, the default deletion strategy is
+ # For <tt>has_many :through</tt> associations, the default deletion strategy is
# +:delete_all+.
#
# For +has_many+ associations, the default deletion strategy is +:nullify+.
@@ -532,7 +557,7 @@ module ActiveRecord
# # => [#<Pet id: 2, name: "Spook", person_id: 1>]
#
# Pet.find(1, 3)
- # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3)
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 3)
#
# If it is set to <tt>:delete_all</tt>, all the +records+ are deleted
# *without* calling their +destroy+ method.
@@ -560,7 +585,7 @@ module ActiveRecord
# # ]
#
# Pet.find(1)
- # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=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.
@@ -624,7 +649,7 @@ module ActiveRecord
# person.pets.size # => 0
# person.pets # => []
#
- # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3)
+ # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (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.
@@ -656,7 +681,7 @@ module ActiveRecord
# person.pets.size # => 0
# person.pets # => []
#
- # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6)
+ # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6)
def destroy(*records)
@association.destroy(*records)
end
@@ -781,7 +806,7 @@ module ActiveRecord
# person.pets.any? # => false
#
# person.pets << Pet.new(name: 'Snoop')
- # person.pets.count # => 0
+ # person.pets.count # => 1
# person.pets.any? # => true
#
# You can also pass a +block+ to define criteria. The behavior
@@ -856,7 +881,7 @@ module ActiveRecord
!!@association.include?(record)
end
- def arel
+ def arel #:nodoc:
scope.arel
end
@@ -971,7 +996,7 @@ module ActiveRecord
alias_method :append, :<<
def prepend(*args)
- raise NoMethodError, "prepend on association is not defined. Please use << or append"
+ raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
end
# Equivalent to +delete_all+. The difference is that returns +self+, instead
diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb
index fe48ecec29..3ceec0ee46 100644
--- a/activerecord/lib/active_record/associations/foreign_association.rb
+++ b/activerecord/lib/active_record/associations/foreign_association.rb
@@ -1,5 +1,5 @@
module ActiveRecord::Associations
- module ForeignAssociation
+ module ForeignAssociation # :nodoc:
def foreign_key_present?
if reflection.klass.primary_key
owner.attribute_present?(reflection.active_record_primary_key)
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index ca27c9fdde..a9f6aaafef 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -15,8 +15,15 @@ module ActiveRecord
when :restrict_with_error
unless empty?
- record = klass.human_attribute_name(reflection.name).downcase
- owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record)
+ record = owner.class.human_attribute_name(reflection.name).downcase
+ message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.many', record: record, raise: true) rescue nil
+ if message
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ The error key `:'restrict_dependent_destroy.many'` has been deprecated and will be removed in Rails 5.1.
+ Please use `:'restrict_dependent_destroy.has_many'` instead.
+ MESSAGE
+ end
+ owner.errors.add(:base, message || :'restrict_dependent_destroy.has_many', record: record)
throw(:abort)
end
@@ -43,7 +50,7 @@ module ActiveRecord
end
def empty?
- if has_cached_counter?
+ if reflection.has_cached_counter?
size.zero?
else
super
@@ -66,8 +73,8 @@ module ActiveRecord
# 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 has_cached_counter?
- owner._read_attribute cached_counter_attribute_name
+ count = if reflection.has_cached_counter?
+ owner._read_attribute reflection.counter_cache_column
else
scope.count
end
@@ -80,70 +87,20 @@ module ActiveRecord
[association_scope.limit_value, count].compact.min
end
- def has_cached_counter?(reflection = reflection())
- owner.attribute_present?(cached_counter_attribute_name(reflection))
- end
-
- def cached_counter_attribute_name(reflection = reflection())
- if reflection.options[:counter_cache]
- reflection.options[:counter_cache].to_s
- else
- "#{reflection.name}_count"
- end
- end
-
def update_counter(difference, reflection = reflection())
- update_counter_in_database(difference, reflection)
- update_counter_in_memory(difference, reflection)
- end
-
- def update_counter_in_database(difference, reflection = reflection())
- if has_cached_counter?(reflection)
- counter = cached_counter_attribute_name(reflection)
- owner.class.update_counters(owner.id, counter => difference)
+ if reflection.has_cached_counter?
+ owner.increment!(reflection.counter_cache_column, difference)
end
end
def update_counter_in_memory(difference, reflection = reflection())
- if counter_must_be_updated_by_has_many?(reflection)
- counter = cached_counter_attribute_name(reflection)
- owner[counter] += difference
- owner.send(:clear_attribute_changes, counter) # eww
+ if reflection.counter_must_be_updated_by_has_many?
+ counter = reflection.counter_cache_column
+ owner.increment(counter, difference)
+ owner.send(:clear_attribute_change, counter) # eww
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(reflection = reflection())
- counter_name = cached_counter_attribute_name(reflection)
- inverse_which_updates_counter_named(counter_name, reflection)
- end
- alias inverse_updates_counter_cache? inverse_which_updates_counter_cache
-
- def inverse_which_updates_counter_named(counter_name, reflection)
- reflection.klass._reflections.values.find { |inverse_reflection|
- inverse_reflection.belongs_to? &&
- inverse_reflection.counter_cache_column == counter_name
- }
- end
-
- def inverse_updates_counter_in_memory?(reflection)
- inverse = inverse_which_updates_counter_cache(reflection)
- inverse && inverse == reflection.inverse_of
- end
-
- def counter_must_be_updated_by_has_many?(reflection)
- !inverse_updates_counter_in_memory?(reflection) && has_cached_counter?(reflection)
- end
-
def delete_count(method, scope)
if method == :delete_all
scope.delete_all
@@ -161,7 +118,7 @@ module ActiveRecord
def delete_records(records, method)
if method == :destroy
records.each(&:destroy!)
- update_counter(-records.length) unless inverse_updates_counter_cache?
+ 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))
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index cd79266952..deb0f8c9f5 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -110,7 +110,7 @@ module ActiveRecord
def update_through_counter?(method)
case method
when :destroy
- !inverse_updates_counter_cache?(through_reflection)
+ !through_reflection.inverse_updates_counter_cache?
when :nullify
false
else
@@ -133,7 +133,7 @@ module ActiveRecord
if scope.klass.primary_key
count = scope.destroy_all.length
else
- scope.each { |record| record.run_callbacks :destroy }
+ scope.each(&:_run_destroy_callbacks)
arel = scope.arel
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 41a75b820e..0fe9b2e81b 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- # = Active Record Belongs To Has One Association
+ # = Active Record Has One Association
module Associations
class HasOneAssociation < SingularAssociation #:nodoc:
include ForeignAssociation
@@ -11,8 +11,15 @@ module ActiveRecord
when :restrict_with_error
if load_target
- record = klass.human_attribute_name(reflection.name).downcase
- owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record)
+ record = owner.class.human_attribute_name(reflection.name).downcase
+ message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.one', record: record, raise: true) rescue nil
+ if message
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ The error key `:'restrict_dependent_destroy.one'` has been deprecated and will be removed in Rails 5.1.
+ Please use `:'restrict_dependent_destroy.has_one'` instead.
+ MESSAGE
+ end
+ owner.errors.add(:base, message || :'restrict_dependent_destroy.has_one', record: record)
throw(:abort)
end
@@ -58,7 +65,7 @@ module ActiveRecord
when :destroy
target.destroy
when :nullify
- target.update_columns(reflection.foreign_key => nil)
+ target.update_columns(reflection.foreign_key => nil) if target.persisted?
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 81eb5136a1..9f183c3e7e 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -103,9 +103,14 @@ module ActiveRecord
join_root.drop(1).map!(&:reflection)
end
- def join_constraints(outer_joins)
+ def join_constraints(outer_joins, join_type)
joins = join_root.children.flat_map { |child|
- make_inner_joins join_root, child
+
+ if join_type == Arel::Nodes::OuterJoin
+ make_left_outer_joins join_root, child
+ else
+ make_inner_joins join_root, child
+ end
}
joins.concat outer_joins.flat_map { |oj|
@@ -131,9 +136,9 @@ module ActiveRecord
def instantiate(result_set, aliases)
primary_key = aliases.column_alias(join_root, join_root.primary_key)
- seen = Hash.new { |h,parent_klass|
- h[parent_klass] = Hash.new { |i,parent_id|
- i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} }
+ seen = Hash.new { |i, object_id|
+ i[object_id] = Hash.new { |j, child_class|
+ j[child_class] = {}
}
}
@@ -150,7 +155,8 @@ module ActiveRecord
message_bus.instrument('instantiation.active_record', payload) do
result_set.each { |row_hash|
- parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases)
+ parent_key = primary_key ? row_hash[primary_key] : row_hash
+ parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases)
construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
}
end
@@ -175,6 +181,14 @@ module ActiveRecord
[info] + child.children.flat_map { |c| make_outer_joins(child, c) }
end
+ def make_left_outer_joins(parent, child)
+ tables = child.tables
+ join_type = Arel::Nodes::OuterJoin
+ info = make_constraints parent, child, tables, join_type
+
+ [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) }
+ end
+
def make_inner_joins(parent, child)
tables = child.tables
join_type = Arel::Nodes::InnerJoin
@@ -233,7 +247,6 @@ module ActiveRecord
def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
return if ar_parent.nil?
- primary_id = ar_parent.id
parent.children.each do |node|
if node.reflection.collection?
@@ -253,14 +266,14 @@ module ActiveRecord
next
end
- model = seen[parent.base_klass][primary_id][node.base_klass][id]
+ model = seen[ar_parent.object_id][node.base_klass][id]
if model
construct(model, node, row, rs, seen, model_cache, aliases)
else
model = construct_model(ar_parent, node, row, model_cache, id, aliases)
model.readonly!
- seen[parent.base_klass][primary_id][node.base_klass][id] = model
+ seen[ar_parent.object_id][node.base_klass][id] = model
construct(model, node, row, rs, seen, model_cache, aliases)
end
end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index d60dd911e1..ecf6fb8643 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -54,6 +54,8 @@ module ActiveRecord
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
end
+ NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, [])
+
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
@@ -88,9 +90,6 @@ module ActiveRecord
# [ :books, :author ]
# { author: :avatar }
# [ :books, { author: :avatar } ]
-
- NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, [])
-
def preload(records, associations, preload_scope = nil)
records = Array.wrap(records).compact.uniq
associations = Array.wrap(associations)
@@ -107,6 +106,7 @@ module ActiveRecord
private
+ # Loads all the given data into +records+ for the +association+.
def preloaders_on(association, records, scope)
case association
when Hash
@@ -116,7 +116,7 @@ module ActiveRecord
when String
preloaders_for_one(association.to_sym, records, scope)
else
- raise ArgumentError, "#{association.inspect} was not recognised for preload"
+ raise ArgumentError, "#{association.inspect} was not recognized for preload"
end
end
@@ -132,6 +132,11 @@ module ActiveRecord
}
end
+ # Loads all the given data into +records+ for a singular +association+.
+ #
+ # Functions by instantiating a preloader class such as Preloader::HasManyThrough and
+ # call the +run+ method for each passed in class in the +records+ argument.
+ #
# Not all records have the same class, so group then preload group on the reflection
# itself so that if various subclass share the same association then we do not split
# them unnecessarily
@@ -160,7 +165,7 @@ module ActiveRecord
h
end
- class AlreadyLoaded
+ class AlreadyLoaded # :nodoc:
attr_reader :owners, :reflection
def initialize(klass, owners, reflection, preload_scope)
@@ -175,12 +180,16 @@ module ActiveRecord
end
end
- class NullPreloader
+ class NullPreloader # :nodoc:
def self.new(klass, owners, reflection, preload_scope); self; end
def self.run(preloader); end
def self.preloaded_records; []; end
end
+ # Returns a class containing the logic needed to load preload the data
+ # and attach it to a relation. For example +Preloader::Association+ or
+ # +Preloader::HasManyThrough+. The class returned implements a `run` method
+ # that accepts a preloader.
def preloader_for(reflection, owners, rhs_klass)
return NullPreloader unless rhs_klass
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 1dc8bff193..e11a5cfb8a 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -12,7 +12,6 @@ module ActiveRecord
@preload_scope = preload_scope
@model = owners.first && owners.first.class
@scope = nil
- @owners_by_key = nil
@preloaded_records = []
end
@@ -56,18 +55,6 @@ module ActiveRecord
raise NotImplementedError
end
- def owners_by_key
- @owners_by_key ||= if key_conversion_required?
- owners.group_by do |owner|
- owner[owner_key_name].to_s
- end
- else
- owners.group_by do |owner|
- owner[owner_key_name]
- end
- end
- end
-
def options
reflection.options
end
@@ -75,32 +62,33 @@ module ActiveRecord
private
def associated_records_by_owner(preloader)
- owners_map = owners_by_key
- owner_keys = owners_map.keys.compact
-
- # Each record may have multiple owners, and vice-versa
- records_by_owner = owners.each_with_object({}) do |owner,h|
- h[owner] = []
+ records = load_records
+ owners.each_with_object({}) do |owner, result|
+ result[owner] = records[convert_key(owner[owner_key_name])] || []
end
+ end
- if owner_keys.any?
- # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
- # Make several smaller queries if necessary or make one query if the adapter supports it
- sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
-
- records = load_slices sliced
- records.each do |record, owner_key|
- owners_map[owner_key].each do |owner|
- records_by_owner[owner] << record
- end
+ def owner_keys
+ unless defined?(@owner_keys)
+ @owner_keys = owners.map do |owner|
+ owner[owner_key_name]
end
+ @owner_keys.uniq!
+ @owner_keys.compact!
end
-
- records_by_owner
+ @owner_keys
end
def key_conversion_required?
- association_key_type != owner_key_type
+ @key_conversion_required ||= association_key_type != owner_key_type
+ end
+
+ def convert_key(key)
+ if key_conversion_required?
+ key.to_s
+ else
+ key
+ end
end
def association_key_type
@@ -111,17 +99,17 @@ module ActiveRecord
@model.type_for_attribute(owner_key_name.to_s).type
end
- def load_slices(slices)
- @preloaded_records = slices.flat_map { |slice|
+ def load_records
+ return {} if owner_keys.empty?
+ # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
+ # Make several smaller queries if necessary or make one query if the adapter supports it
+ slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
+ @preloaded_records = slices.flat_map do |slice|
records_for(slice)
- }
-
- @preloaded_records.map { |record|
- key = record[association_key_name]
- key = key.to_s if key_conversion_required?
-
- [record, key]
- }
+ end
+ @preloaded_records.group_by do |record|
+ convert_key(record[association_key_name])
+ end
end
def reflection_scope
@@ -137,14 +125,23 @@ module ActiveRecord
scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause
scope.references_values = Array(values[:references]) + Array(preload_values[:references])
- scope._select! preload_values[:select] || values[:select] || table[Arel.star]
+ if preload_values[:select] || values[:select]
+ scope._select!(preload_values[:select] || values[:select])
+ end
scope.includes! preload_values[:includes] || values[:includes]
if preload_scope.joins_values.any?
scope.joins!(preload_scope.joins_values)
else
scope.joins!(reflection_scope.joins_values)
end
- scope.order! preload_values[:order] || values[:order]
+
+ if order_values = preload_values[:order] || values[:order]
+ scope.order!(order_values)
+ end
+
+ if preload_values[:reordering] || values[:reordering]
+ scope.reordering_value = true
+ end
if preload_values[:readonly] || values[:readonly]
scope.readonly!
@@ -154,7 +151,7 @@ module ActiveRecord
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
- scope.unscope_values = Array(values[:unscope])
+ scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope])
klass.default_scoped.merge(scope)
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
index 5adffcd831..9939280fa4 100644
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb
@@ -2,13 +2,8 @@ module ActiveRecord
module Associations
class Preloader
class CollectionAssociation < Association #:nodoc:
-
private
- def build_scope
- super.order(preload_scope.values[:order] || reflection_scope.values[:order])
- end
-
def preload(preloader)
associated_records_by_owner(preloader).each do |owner, records|
association = owner.association(reflection.name)
@@ -17,7 +12,6 @@ module ActiveRecord
records.each { |record| association.set_inverse_instance(record) }
end
end
-
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb
index 24728e9f01..c4add621ca 100644
--- a/activerecord/lib/active_record/associations/preloader/has_one.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_one.rb
@@ -2,7 +2,6 @@ module ActiveRecord
module Associations
class Preloader
class HasOne < SingularAssociation #:nodoc:
-
def association_key_name
reflection.foreign_key
end
@@ -10,13 +9,6 @@ module ActiveRecord
def owner_key_name
reflection.active_record_primary_key
end
-
- private
-
- def build_scope
- super.order(preload_scope.values[:order] || reflection_scope.values[:order])
- end
-
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 56aa23b173..24aa47bb51 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -84,7 +84,9 @@ module ActiveRecord
end
scope.references! reflection_scope.values[:references]
- scope = scope.order reflection_scope.values[:order] if scope.eager_loading?
+ if scope.eager_loading? && order_values = reflection_scope.values[:order]
+ scope = scope.order(order_values)
+ end
end
scope
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index bec9505bd2..c7cc48ba16 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -4,6 +4,12 @@ module ActiveRecord
# Implements the reader method, e.g. foo.bar for Foo.has_one :bar
def reader(force_reload = false)
if force_reload && klass
+ 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 parent object instead.
+ MSG
+
klass.uncached { reload }
elsif !loaded? || stale_target?
reload
@@ -39,12 +45,7 @@ module ActiveRecord
end
def get_records
- if reflection.scope_chain.any?(&:any?) ||
- scope.eager_loading? ||
- klass.scope_attributes?
-
- return scope.limit(1).to_a
- end
+ return scope.limit(1).to_a if skip_statement_cache?
conn = klass.connection
sc = reflection.association_scope_cache(conn, owner) do
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index af1bce523c..d0ec3e8015 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -15,12 +15,6 @@ module ActiveRecord
scope = super
reflection.chain.drop(1).each do |reflection|
relation = reflection.klass.all
-
- reflection_scope = reflection.scope
- if reflection_scope && reflection_scope.arity.zero?
- relation = relation.merge(reflection_scope)
- end
-
scope.merge!(
relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
@@ -82,13 +76,21 @@ module ActiveRecord
def ensure_mutable
unless source_reflection.belongs_to?
- raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ if reflection.has_one?
+ raise HasOneThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ else
+ raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ end
end
end
def ensure_not_nested
if reflection.nested?
- raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ if reflection.has_one?
+ raise HasOneThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ else
+ raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ end
end
end