Add `ActiveRecord::Base.with_connection` as a shortcut

Extracted from https://github.com/rails/rails/pull/50793

The leased connection is yielded, and for the duration of the block,
any call to `ActiveRecord::Base.connection` will yield that same connection.

This is useful to perform a few database operations without causing a
connection to be leased for the entire duration of the request or job.
This commit is contained in:
Jean Boussier 2024-02-14 13:55:34 +01:00
parent bbd2be4e99
commit 22f41a1b40
6 changed files with 98 additions and 24 deletions

View File

@ -1,3 +1,13 @@
* Add `ActiveRecord::Base.with_connection` as a shortcut for leasing a connection for a short duration.
The leased connection is yielded, and for the duration of the block, any call to `ActiveRecord::Base.connection`
will yield that same connection.
This is useful to perform a few database operations without causing a connection to be leased for the
entire duration of the request or job.
*Jean Boussier*
* Deprecate `config.active_record.warn_on_records_fetched_greater_than` now that `sql.active_record`
notification includes `:row_count` field.

View File

@ -42,8 +42,8 @@ module ActiveRecord::Associations::Builder # :nodoc:
self.right_reflection = _reflect_on_association(rhs_name)
end
def self.retrieve_connection
left_model.retrieve_connection
def self.connection_pool
left_model.connection_pool
end
}

View File

@ -182,7 +182,10 @@ module ActiveRecord
role = ActiveRecord::Base.current_role
end
each_connection_pool(role).each(&:release_connection)
each_connection_pool(role).each do |pool|
pool.release_connection
pool.disable_query_cache!
end
end
# Clears the cache which maps classes.
@ -223,20 +226,7 @@ module ActiveRecord
# opened and set as the active connection for the class it was defined
# for (not necessarily the current class).
def retrieve_connection(connection_name, role: ActiveRecord::Base.current_role, shard: ActiveRecord::Base.current_shard) # :nodoc:
pool = retrieve_connection_pool(connection_name, role: role, shard: shard)
unless pool
if shard != ActiveRecord::Base.default_shard
message = "No connection pool for '#{connection_name}' found for the '#{shard}' shard."
elsif role != ActiveRecord::Base.default_role
message = "No connection pool for '#{connection_name}' found for the '#{role}' role."
else
message = "No connection pool for '#{connection_name}' found."
end
raise ConnectionNotEstablished, message
end
pool = retrieve_connection_pool(connection_name, role: role, shard: shard, strict: true)
pool.connection
end
@ -256,9 +246,22 @@ module ActiveRecord
# Retrieving the connection pool happens a lot, so we cache it in @connection_name_to_pool_manager.
# This makes retrieving the connection pool O(1) once the process is warm.
# When a connection is established or removed, we invalidate the cache.
def retrieve_connection_pool(connection_name, role: ActiveRecord::Base.current_role, shard: ActiveRecord::Base.current_shard)
pool_config = get_pool_manager(connection_name)&.get_pool_config(role, shard)
pool_config&.pool
def retrieve_connection_pool(connection_name, role: ActiveRecord::Base.current_role, shard: ActiveRecord::Base.current_shard, strict: false)
pool = get_pool_manager(connection_name)&.get_pool_config(role, shard)&.pool
if strict && !pool
if shard != ActiveRecord::Base.default_shard
message = "No connection pool for '#{connection_name}' found for the '#{shard}' shard."
elsif role != ActiveRecord::Base.default_role
message = "No connection pool for '#{connection_name}' found for the '#{role}' role."
else
message = "No connection pool for '#{connection_name}' found."
end
raise ConnectionNotEstablished, message
end
pool
end
private

View File

@ -243,7 +243,7 @@ module ActiveRecord
# Clears the query cache for all connections associated with the current thread.
def clear_query_caches_for_current_thread
connection_handler.each_connection_pool do |pool|
pool.connection.clear_query_cache if pool.active_connection?
pool.connection.clear_query_cache
end
end
@ -251,7 +251,14 @@ module ActiveRecord
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
def connection
retrieve_connection
connection_pool.connection
end
# Checkouts a connection from the pool, yield it and then check it back in.
# If a connection was already leased via #connection or a parent call to
# #with_connection, that same connection is yieled.
def with_connection(&block) # :nodoc:
connection_pool.with_connection(&block)
end
attr_writer :connection_specification_name
@ -280,7 +287,7 @@ module ActiveRecord
end
def connection_pool
connection_handler.retrieve_connection_pool(connection_specification_name, role: current_role, shard: current_shard) || raise(ConnectionNotEstablished)
connection_handler.retrieve_connection_pool(connection_specification_name, role: current_role, shard: current_shard, strict: true)
end
def retrieve_connection

View File

@ -209,7 +209,9 @@ module ActiveRecord
module ClassMethods
# See the ConnectionAdapters::DatabaseStatements#transaction API docs.
def transaction(**options, &block)
connection.transaction(**options, &block)
with_connection do |connection|
connection.transaction(**options, &block)
end
end
def before_commit(*args, &block) # :nodoc:

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
require "cases/helper"
module ActiveRecord
class ConnectionHandlingTest < ActiveRecord::TestCase
unless in_memory_db?
test "#with_connection lease the connection for the duration of the block" do
ActiveRecord::Base.connection_pool.release_connection
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
ActiveRecord::Base.with_connection do |connection|
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
assert_same connection, ActiveRecord::Base.connection
end
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
end
test "#with_connection use the already leased connection if available" do
leased_connection = ActiveRecord::Base.connection
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
ActiveRecord::Base.with_connection do |connection|
assert_same leased_connection, connection
assert_same ActiveRecord::Base.connection, connection
end
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
assert_same ActiveRecord::Base.connection, leased_connection
end
test "#with_connection is reentrant" do
leased_connection = ActiveRecord::Base.connection
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
ActiveRecord::Base.with_connection do |connection|
assert_same leased_connection, connection
assert_same ActiveRecord::Base.connection, connection
ActiveRecord::Base.with_connection do |connection2|
assert_same leased_connection, connection
assert_same ActiveRecord::Base.connection, connection
end
end
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
assert_same ActiveRecord::Base.connection, leased_connection
end
end
end
end