aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md12
-rw-r--r--activerecord/lib/active_record/association_relation.rb5
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb8
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb20
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb1
-rw-r--r--activerecord/lib/active_record/autosave_association.rb4
-rw-r--r--activerecord/lib/active_record/core.rb20
-rw-r--r--activerecord/lib/active_record/persistence.rb12
-rw-r--r--activerecord/lib/active_record/querying.rb4
-rw-r--r--activerecord/lib/active_record/relation.rb14
-rw-r--r--activerecord/lib/active_record/statement_cache.rb4
-rw-r--r--activerecord/lib/active_record/table_metadata.rb4
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb6
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb27
-rw-r--r--activerecord/test/cases/query_cache_test.rb2
18 files changed, 110 insertions, 44 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 4aa1853bde..f62d29b5bf 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,15 @@
+* Inverse association instances will now be set before `after_find` or
+ `after_initialize` callbacks are run.
+
+ Fixes #26320.
+
+ *Sean Griffin*
+
+* Remove unnecessarily association load when a `belongs_to` association has already been
+ loaded then the foreign key is changed directly and the record saved.
+
+ *James Coleman*
+
* Remove standardized column types/arguments spaces in schema dump.
*Tim Petricola*
diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
index 2da2d968b9..de2d03cd0b 100644
--- a/activerecord/lib/active_record/association_relation.rb
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -29,7 +29,10 @@ module ActiveRecord
private
def exec_queries
- super.each { |r| @association.set_inverse_instance r }
+ super do |r|
+ @association.set_inverse_instance r
+ yield r if block_given?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 0f51b35164..0c911a5396 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -390,9 +390,9 @@ module ActiveRecord
end
binds = AssociationScope.get_bind_values(owner, reflection.chain)
- records = sc.execute(binds, klass, conn)
- records.each { |record| set_inverse_instance(record) }
- records
+ sc.execute(binds, klass, conn) do |record|
+ set_inverse_instance(record)
+ end
end
# We have some records loaded from the database (persisted) and some that are
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 62acad0eda..02f0721bed 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -286,17 +286,19 @@ module ActiveRecord
end
def construct_model(record, node, row, model_cache, id, aliases)
- model = model_cache[node][id] ||= node.instantiate(row,
- aliases.column_aliases(node))
other = record.association(node.reflection.name)
+ model = model_cache[node][id] ||=
+ node.instantiate(row, aliases.column_aliases(node)) do |m|
+ other.set_inverse_instance(m)
+ end
+
if node.reflection.collection?
other.target.push(model)
else
other.target = model
end
- other.set_inverse_instance(model)
model
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
index 551087f822..61cec5403a 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -62,8 +62,8 @@ module ActiveRecord
hash
end
- def instantiate(row, aliases)
- base_klass.instantiate(extract_record(row, aliases))
+ def instantiate(row, aliases, &block)
+ base_klass.instantiate(extract_record(row, aliases), &block)
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 4bb627f399..07407700cd 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -62,7 +62,12 @@ module ActiveRecord
private
def associated_records_by_owner(preloader)
- records = load_records
+ records = load_records do |record|
+ owner = owners_by_key[convert_key(record[association_key_name])]
+ association = owner.association(reflection.name)
+ association.set_inverse_instance(record)
+ end
+
owners.each_with_object({}) do |owner, result|
result[owner] = records[convert_key(owner[owner_key_name])] || []
end
@@ -79,6 +84,15 @@ module ActiveRecord
@owner_keys
end
+ def owners_by_key
+ unless defined?(@owners_by_key)
+ @owners_by_key = owners.each_with_object({}) do |owner, h|
+ h[convert_key(owner[owner_key_name])] = owner
+ end
+ end
+ @owners_by_key
+ end
+
def key_conversion_required?
@key_conversion_required ||= association_key_type != owner_key_type
end
@@ -99,13 +113,13 @@ module ActiveRecord
@model.type_for_attribute(owner_key_name.to_s).type
end
- def load_records
+ def load_records(&block)
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)
+ records_for(slice).load(&block)
end
@preloaded_records.group_by do |record|
convert_key(record[association_key_name])
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
index 24b8e01029..26690bf16d 100644
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb
@@ -9,7 +9,6 @@ module ActiveRecord
association = owner.association(reflection.name)
association.loaded!
association.target.concat(records)
- records.each { |record| association.set_inverse_instance(record) }
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
index 0888d383a6..5c5828262e 100644
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb
@@ -10,7 +10,6 @@ module ActiveRecord
association = owner.association(reflection.name)
association.target = record
- association.set_inverse_instance(record) if record
end
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 6e6620aad5..db84876b0a 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -457,7 +457,9 @@ module ActiveRecord
# 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
+ return unless association && association.loaded? && !association.stale_target?
+
+ record = association.load_target
if record && !record.destroyed?
autosave = reflection.options[:autosave]
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index cb11cdefd9..2725c85446 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -366,6 +366,8 @@ module ActiveRecord
self.class.define_attribute_methods
+ yield self if block_given?
+
_run_find_callbacks
_run_initialize_callbacks
@@ -541,15 +543,15 @@ module ActiveRecord
private
- # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements
- # of the array, and then rescues from the possible NoMethodError. If those elements are
- # ActiveRecord::Base's, then this triggers the various method_missing's that we have,
- # which significantly impacts upon performance.
- #
- # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here.
- #
- # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
- def to_ary # :nodoc:
+ # +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of
+ # the array, and then rescues from the possible +NoMethodError+. If those elements are
+ # +ActiveRecord::Base+'s, then this triggers the various +method_missing+'s that we have,
+ # which significantly impacts upon performance.
+ #
+ # So we can avoid the +method_missing+ hit by explicitly defining +#to_ary+ as +nil+ here.
+ #
+ # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
+ def to_ary
nil
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index dc4af4f390..a04ef2e263 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -63,10 +63,10 @@ module ActiveRecord
#
# See <tt>ActiveRecord::Inheritance#discriminate_class_for_record</tt> to see
# how this "single-table" inheritance mapping is implemented.
- def instantiate(attributes, column_types = {})
+ def instantiate(attributes, column_types = {}, &block)
klass = discriminate_class_for_record(attributes)
attributes = klass.attributes_builder.build_from_database(attributes, column_types)
- klass.allocate.init_with("attributes" => attributes, "new_record" => false)
+ klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block)
end
private
@@ -178,7 +178,7 @@ module ActiveRecord
# and #destroy returns +false+.
# See ActiveRecord::Callbacks for further details.
def destroy
- raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
+ _raise_readonly_record_error if readonly?
destroy_associations
self.class.connection.add_transaction_record(self)
destroy_row if persisted?
@@ -535,7 +535,7 @@ module ActiveRecord
end
def create_or_update(*args)
- raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
+ _raise_readonly_record_error if readonly?
result = new_record? ? _create_record : _update_record(*args)
result != false
end
@@ -577,5 +577,9 @@ module ActiveRecord
def belongs_to_touch_method
:touch
end
+
+ def _raise_readonly_record_error
+ raise ReadOnlyRecord, "#{self.class} is marked as readonly"
+ end
end
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index dd7d650207..36689f6559 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -35,7 +35,7 @@ module ActiveRecord
#
# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
# Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }]
- def find_by_sql(sql, binds = [], preparable: nil)
+ def find_by_sql(sql, binds = [], preparable: nil, &block)
result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable)
column_types = result_set.column_types.dup
columns_hash.each_key { |k| column_types.delete k }
@@ -47,7 +47,7 @@ module ActiveRecord
}
message_bus.instrument("instantiation.active_record", payload) do
- result_set.map { |record| instantiate(record, column_types) }
+ result_set.map { |record| instantiate(record, column_types, &block) }
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 85b1ddf8db..6d571cf026 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -29,9 +29,7 @@ module ActiveRecord
end
def initialize_copy(other)
- # This method is a hot spot, so for now, use Hash[] to dup the hash.
- # https://bugs.ruby-lang.org/issues/7166
- @values = Hash[@values]
+ @values = @values.dup
reset
end
@@ -564,8 +562,8 @@ module ActiveRecord
# return value is the relation itself, not the records.
#
# Post.where(published: true).load # => #<ActiveRecord::Relation>
- def load
- exec_queries unless loaded?
+ def load(&block)
+ exec_queries(&block) unless loaded?
self
end
@@ -661,7 +659,7 @@ module ActiveRecord
end
def values
- Hash[@values]
+ @values.dup
end
def inspect
@@ -680,8 +678,8 @@ module ActiveRecord
private
- def exec_queries
- @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes).freeze
+ def exec_queries(&block)
+ @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes, &block).freeze
preload = preload_values
preload += includes_values unless eager_loading?
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index fd67032235..d19bb96ede 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -99,12 +99,12 @@ module ActiveRecord
@bind_map = bind_map
end
- def execute(params, klass, connection)
+ def execute(params, klass, connection, &block)
bind_values = bind_map.bind params
sql = query_builder.sql_for bind_values, connection
- klass.find_by_sql(sql, bind_values, preparable: true)
+ klass.find_by_sql(sql, bind_values, preparable: true, &block)
end
alias :call :execute
end
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index a398e0cb7d..58184f3872 100644
--- a/activerecord/lib/active_record/table_metadata.rb
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -10,9 +10,7 @@ module ActiveRecord
end
def resolve_column_aliases(hash)
- # This method is a hot spot, so for now, use Hash[] to dup the hash.
- # https://bugs.ruby-lang.org/issues/7166
- new_hash = Hash[hash]
+ new_hash = hash.dup
hash.each do |key, _|
if (key.is_a?(Symbol)) && klass.attribute_alias?(key)
new_hash[klass.attribute_alias(key)] = new_hash.delete(key)
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 3f42cb9b9d..2418346d1b 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -626,6 +626,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_queries(0) { tagging.super_tag }
end
+ def test_dont_find_target_when_saving_foreign_key_after_stale_association_loaded
+ client = Client.create!(name: "Test client", firm_with_basic_id: Firm.find(1))
+ client.firm_id = Firm.create!(name: "Test firm").id
+ assert_queries(1) { client.save! }
+ end
+
def test_field_name_same_as_foreign_key
computer = Computer.find(1)
assert_not_nil computer.developer, ":foreign key == attribute didn't lock up" # '
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 0b23cea420..6fe6ee6783 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -494,6 +494,33 @@ class InverseHasManyTests < ActiveRecord::TestCase
assert !man.persisted?
end
+
+ def test_inverse_instance_should_be_set_before_find_callbacks_are_run
+ reset_callbacks(Interest, :find) do
+ Interest.after_find { raise unless association(:man).loaded? && man.present? }
+
+ assert Man.first.interests.reload.any?
+ assert Man.includes(:interests).first.interests.any?
+ assert Man.joins(:interests).includes(:interests).first.interests.any?
+ end
+ end
+
+ def test_inverse_instance_should_be_set_before_initialize_callbacks_are_run
+ reset_callbacks(Interest, :initialize) do
+ Interest.after_initialize { raise unless association(:man).loaded? && man.present? }
+
+ assert Man.first.interests.reload.any?
+ assert Man.includes(:interests).first.interests.any?
+ assert Man.joins(:interests).includes(:interests).first.interests.any?
+ end
+ end
+
+ def reset_callbacks(target, type)
+ old_callbacks = target.send(:get_callbacks, type).deep_dup
+ yield
+ ensure
+ target.send(:set_callbacks, type, old_callbacks) if old_callbacks
+ end
end
class InverseBelongsToTests < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 619a5f4f3f..7f7faca70d 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -215,7 +215,7 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_query_cached_even_when_types_are_reset
Task.cache do
# Warm the cache
- task = Task.find(1)
+ Task.find(1)
Task.connection.type_map.clear