# frozen_string_literal: true
module ActiveRecord
module ConnectionHandling
RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence }
DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" }
# Establishes the connection to the database. Accepts a hash as input where
# the :adapter key must be specified with the name of a database adapter (in lower-case)
# example for regular databases (MySQL, PostgreSQL, etc):
#
# ActiveRecord::Base.establish_connection(
# adapter: "mysql2",
# host: "localhost",
# username: "myuser",
# password: "mypass",
# database: "somedatabase"
# )
#
# Example for SQLite database:
#
# ActiveRecord::Base.establish_connection(
# adapter: "sqlite3",
# database: "path/to/dbfile"
# )
#
# Also accepts keys as strings (for parsing from YAML for example):
#
# ActiveRecord::Base.establish_connection(
# "adapter" => "sqlite3",
# "database" => "path/to/dbfile"
# )
#
# Or a URL:
#
# ActiveRecord::Base.establish_connection(
# "postgres://myuser:mypass@localhost/somedatabase"
# )
#
# In case {ActiveRecord::Base.configurations}[rdoc-ref:Core.configurations]
# is set (Rails automatically loads the contents of config/database.yml into it),
# a symbol can also be given as argument, representing a key in the
# configuration hash:
#
# ActiveRecord::Base.establish_connection(:production)
#
# The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+
# may be returned on an error.
def establish_connection(config_or_env = nil)
config_or_env ||= DEFAULT_ENV.call.to_sym
db_config, owner_name = resolve_config_for_connection(config_or_env)
connection_handler.establish_connection(db_config, owner_name: owner_name, role: current_role, shard: current_shard)
end
# Connects a model to the databases specified. The +database+ keyword
# takes a hash consisting of a +role+ and a +database_key+.
#
# This will create a connection handler for switching between connections,
# look up the config hash using the +database_key+ and finally
# establishes a connection to that config.
#
# class AnimalsModel < ApplicationRecord
# self.abstract_class = true
#
# connects_to database: { writing: :primary, reading: :primary_replica }
# end
#
# +connects_to+ also supports horizontal sharding. The horizontal sharding API
# also supports read replicas. Connect a model to a list of shards like this:
#
# class AnimalsModel < ApplicationRecord
# self.abstract_class = true
#
# connects_to shards: {
# default: { writing: :primary, reading: :primary_replica },
# shard_two: { writing: :primary_shard_two, reading: :primary_shard_replica_two }
# }
# end
#
# Returns an array of database connections.
def connects_to(database: {}, shards: {})
raise NotImplementedError, "`connects_to` can only be called on ActiveRecord::Base or abstract classes" unless self == Base || abstract_class?
if database.present? && shards.present?
raise ArgumentError, "`connects_to` can only accept a `database` or `shards` argument, but not both arguments."
end
connections = []
database.each do |role, database_key|
db_config, owner_name = resolve_config_for_connection(database_key)
handler = lookup_connection_handler(role.to_sym)
self.connection_class = true
connections << handler.establish_connection(db_config, owner_name: owner_name, role: role)
end
shards.each do |shard, database_keys|
database_keys.each do |role, database_key|
db_config, owner_name = resolve_config_for_connection(database_key)
handler = lookup_connection_handler(role.to_sym)
self.connection_class = true
connections << handler.establish_connection(db_config, owner_name: owner_name, role: role, shard: shard.to_sym)
end
end
connections
end
# Connects to a role (ex writing, reading or a custom role) and/or
# shard for the duration of the block. At the end of the block the
# connection will be returned to the original role / shard.
#
# If only a role is passed, Active Record will look up the connection
# based on the requested role. If a non-established role is requested
# an +ActiveRecord::ConnectionNotEstablished+ error will be raised:
#
# ActiveRecord::Base.connected_to(role: :writing) do
# Dog.create! # creates dog using dog writing connection
# end
#
# ActiveRecord::Base.connected_to(role: :reading) do
# Dog.create! # throws exception because we're on a replica
# end
#
# When swapping to a shard, the role must be passed as well. If a non-existent
# shard is passed, an +ActiveRecord::ConnectionNotEstablished+ error will be
# raised.
#
# When a shard and role is passed, Active Record will first lookup the role,
# and then look up the connection by shard key.
#
# ActiveRecord::Base.connected_to(role: :reading, shard: :shard_one_replica) do
# Dog.first # finds first Dog record stored on the shard one replica
# end
#
# The database kwarg is deprecated and will be removed in 6.2.0 without replacement.
def connected_to(database: nil, role: nil, shard: nil, prevent_writes: false, &blk)
if legacy_connection_handling
if self != Base
raise NotImplementedError, "`connected_to` can only be called on ActiveRecord::Base with legacy connection handling."
end
else
if self != Base && !abstract_class
raise NotImplementedError, "calling `connected_to` is only allowed on ActiveRecord::Base or abstract classes."
end
if name != connection_specification_name && !primary_class?
raise NotImplementedError, "calling `connected_to` is only allowed on the abstract class that established the connection."
end
end
if database && (role || shard)
raise ArgumentError, "`connected_to` cannot accept a `database` argument with any other arguments."
elsif database
ActiveSupport::Deprecation.warn("The database key in `connected_to` is deprecated. It will be removed in Rails 6.2.0 without replacement.")
if database.is_a?(Hash)
role, database = database.first
role = role.to_sym
end
db_config, owner_name = resolve_config_for_connection(database)
handler = lookup_connection_handler(role)
handler.establish_connection(db_config, owner_name: owner_name, role: role)
with_handler(role, &blk)
elsif role || shard
unless role
raise ArgumentError, "`connected_to` cannot accept a `shard` argument without a `role`."
end
with_role_and_shard(role, shard, prevent_writes, &blk)
else
raise ArgumentError, "must provide a `shard` and/or `role`."
end
end
# Connects a role and/or shard to the provided connection names. Optionally +prevent_writes+
# can be passed to block writes on a connection. +reading+ will automatically set
# +prevent_writes+ to true.
#
# +connected_to_many+ is an alternative to deeply nested +connected_to+ blocks.
#
# Usage:
#
# ActiveRecord::Base.connected_to_many(AnimalsRecord, MealsRecord, role: :reading) do
# Dog.first # Read from animals replica
# Dinner.first # Read from meals replica
# Person.first # Read from primary writer
# end
def connected_to_many(*classes, role:, shard: nil, prevent_writes: false)
classes = classes.flatten
if legacy_connection_handling
raise NotImplementedError, "connected_to_many is not available with legacy connection handling"
end
if self != Base || classes.include?(Base)
raise NotImplementedError, "connected_to_many can only be called on ActiveRecord::Base."
end
prevent_writes = true if role == reading_role
connected_to_stack << { role: role, shard: shard, prevent_writes: prevent_writes, klasses: classes }
yield
ensure
connected_to_stack.pop
end
# Use a specified connection.
#
# This method is useful for ensuring that a specific connection is
# being used. For example, when booting a console in readonly mode.
#
# It is not recommended to use this method in a request since it
# does not yield to a block like +connected_to+.
def connecting_to(role: default_role, shard: default_shard, prevent_writes: false)
if legacy_connection_handling
raise NotImplementedError, "`connecting_to` is not available with `legacy_connection_handling`."
end
prevent_writes = true if role == reading_role
self.connected_to_stack << { role: role, shard: shard, prevent_writes: prevent_writes, klasses: [self] }
end
# Prevent writing to the database regardless of role.
#
# In some cases you may want to prevent writes to the database
# even if you are on a database that can write. +while_preventing_writes+
# will prevent writes to the database for the duration of the block.
#
# This method does not provide the same protection as a readonly
# user and is meant to be a safeguard against accidental writes.
#
# See +READ_QUERY+ for the queries that are blocked by this
# method.
def while_preventing_writes(enabled = true, &block)
if legacy_connection_handling
connection_handler.while_preventing_writes(enabled, &block)
else
connected_to(role: current_role, prevent_writes: enabled, &block)
end
end
# Returns true if role is the current connected role.
#
# ActiveRecord::Base.connected_to(role: :writing) do
# ActiveRecord::Base.connected_to?(role: :writing) #=> true
# ActiveRecord::Base.connected_to?(role: :reading) #=> false
# end
def connected_to?(role:, shard: ActiveRecord::Base.default_shard)
current_role == role.to_sym && current_shard == shard.to_sym
end
def lookup_connection_handler(handler_key) # :nodoc:
if ActiveRecord::Base.legacy_connection_handling
handler_key ||= ActiveRecord::Base.writing_role
connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new
else
ActiveRecord::Base.connection_handler
end
end
# Clears the query cache for all connections associated with the current thread.
def clear_query_caches_for_current_thread
if ActiveRecord::Base.legacy_connection_handling
ActiveRecord::Base.connection_handlers.each_value do |handler|
clear_on_handler(handler)
end
else
clear_on_handler(ActiveRecord::Base.connection_handler)
end
end
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
def connection
retrieve_connection
end
attr_writer :connection_specification_name
# Return the connection specification name from the current class or its parent.
def connection_specification_name
if !defined?(@connection_specification_name) || @connection_specification_name.nil?
return self == Base ? Base.name : superclass.connection_specification_name
end
@connection_specification_name
end
def primary_class? # :nodoc:
self == Base || defined?(ApplicationRecord) && self == ApplicationRecord
end
# Returns the configuration of the associated connection as a hash:
#
# ActiveRecord::Base.connection_config
# # => {pool: 5, timeout: 5000, database: "db/development.sqlite3", adapter: "sqlite3"}
#
# Please use only for reading.
def connection_config
connection_pool.db_config.configuration_hash
end
deprecate connection_config: "Use connection_db_config instead"
# Returns the db_config object from the associated connection:
#
# ActiveRecord::Base.connection_db_config
# #
#
# Use only for reading.
def connection_db_config
connection_pool.db_config
end
def connection_pool
connection_handler.retrieve_connection_pool(connection_specification_name, role: current_role, shard: current_shard) || raise(ConnectionNotEstablished)
end
def retrieve_connection
connection_handler.retrieve_connection(connection_specification_name, role: current_role, shard: current_shard)
end
# Returns +true+ if Active Record is connected.
def connected?
connection_handler.connected?(connection_specification_name, role: current_role, shard: current_shard)
end
def remove_connection(name = nil)
name ||= @connection_specification_name if defined?(@connection_specification_name)
# if removing a connection that has a pool, we reset the
# connection_specification_name so it will use the parent
# pool.
if connection_handler.retrieve_connection_pool(name, role: current_role, shard: current_shard)
self.connection_specification_name = nil
end
connection_handler.remove_connection_pool(name, role: current_role, shard: current_shard)
end
def clear_cache! # :nodoc:
connection.schema_cache.clear!
end
delegate :clear_active_connections!, :clear_reloadable_connections!,
:clear_all_connections!, :flush_idle_connections!, to: :connection_handler
private
def clear_on_handler(handler)
handler.all_connection_pools.each do |pool|
pool.connection.clear_query_cache if pool.active_connection?
end
end
def resolve_config_for_connection(config_or_env)
raise "Anonymous class is not allowed." unless name
owner_name = primary_class? ? Base.name : name
self.connection_specification_name = owner_name
db_config = Base.configurations.resolve(config_or_env)
[db_config, self]
end
def with_handler(handler_key, &blk)
handler = lookup_connection_handler(handler_key)
swap_connection_handler(handler, &blk)
end
def with_role_and_shard(role, shard, prevent_writes)
prevent_writes = true if role == reading_role
if ActiveRecord::Base.legacy_connection_handling
with_handler(role.to_sym) do
connection_handler.while_preventing_writes(prevent_writes) do
self.connected_to_stack << { shard: shard, klasses: [self] }
yield
end
end
else
self.connected_to_stack << { role: role, shard: shard, prevent_writes: prevent_writes, klasses: [self] }
return_value = yield
return_value.load if return_value.is_a? ActiveRecord::Relation
return_value
end
ensure
self.connected_to_stack.pop
end
def swap_connection_handler(handler, &blk) # :nodoc:
old_handler, ActiveRecord::Base.connection_handler = ActiveRecord::Base.connection_handler, handler
return_value = yield
return_value.load if return_value.is_a? ActiveRecord::Relation
return_value
ensure
ActiveRecord::Base.connection_handler = old_handler
end
end
end