module ActiveRecord::ConnectionHandling
def append_to_connected_to_stack(entry)
def append_to_connected_to_stack(entry) if shard_swapping_prohibited? && entry[:shard].present? raise ArgumentError, "cannot swap `shard` while shard swapping is prohibited." end connected_to_stack << entry end
def clear_cache! # :nodoc:
def clear_cache! # :nodoc: connection.schema_cache.clear! end
def clear_on_handler(handler)
def clear_on_handler(handler) handler.all_connection_pools.each do |pool| pool.connection.clear_query_cache if pool.active_connection? end end
def clear_query_caches_for_current_thread
def clear_query_caches_for_current_thread if ActiveRecord.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
def connected?
def connected? connection_handler.connected?(connection_specification_name, role: current_role, shard: current_shard) end
def connected_to(role: nil, shard: nil, prevent_writes: false, &blk)
Dog.first # finds first Dog record stored on the shard one replica
ActiveRecord::Base.connected_to(role: :reading, shard: :shard_one_replica) do
and then look up the connection by shard key.
When a shard and role is passed, Active Record will first lookup the role,
raised.
shard is passed, an +ActiveRecord::ConnectionNotEstablished+ error will be
When swapping to a shard, the role must be passed as well. If a non-existent
end
Dog.create! # throws exception because we're on a replica
ActiveRecord::Base.connected_to(role: :reading) do
end
Dog.create! # creates dog using dog writing connection
ActiveRecord::Base.connected_to(role: :writing) do
an +ActiveRecord::ConnectionNotEstablished+ error will be raised:
based on the requested role. If a non-established role is requested
If only a role is passed, Active Record will look up the connection
connection will be returned to the original role / shard.
shard for the duration of the block. At the end of the block the
Connects to a role (e.g. writing, reading, or a custom role) and/or
def connected_to(role: nil, shard: nil, prevent_writes: false, &blk) if ActiveRecord.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 unless role || shard raise ArgumentError, "must provide a `shard` and/or `role`." end with_role_and_shard(role, shard, prevent_writes, &blk) end
def connected_to?(role:, shard: ActiveRecord::Base.default_shard)
ActiveRecord::Base.connected_to?(role: :reading) #=> false
ActiveRecord::Base.connected_to?(role: :writing) #=> true
ActiveRecord::Base.connected_to(role: :writing) do
Returns true if role is the current connected role.
def connected_to?(role:, shard: ActiveRecord::Base.default_shard) current_role == role.to_sym && current_shard == shard.to_sym end
def connected_to_many(*classes, role:, shard: nil, prevent_writes: false)
Person.first # Read from primary writer
Dinner.first # Read from meals replica
Dog.first # Read from animals replica
ActiveRecord::Base.connected_to_many(AnimalsRecord, MealsRecord, role: :reading) do
Usage:
+connected_to_many+ is an alternative to deeply nested +connected_to+ blocks.
+prevent_writes+ to true.
can be passed to block writes on a connection. +reading+ will automatically set
Connects a role and/or shard to the provided connection names. Optionally +prevent_writes+
def connected_to_many(*classes, role:, shard: nil, prevent_writes: false) classes = classes.flatten if ActiveRecord.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 == ActiveRecord.reading_role append_to_connected_to_stack(role: role, shard: shard, prevent_writes: prevent_writes, klasses: classes) yield ensure connected_to_stack.pop end
def connecting_to(role: default_role, shard: default_shard, prevent_writes: false)
It is not recommended to use this method in a request since it
being used. For example, when booting a console in readonly mode.
This method is useful for ensuring that a specific connection is
Use a specified connection.
def connecting_to(role: default_role, shard: default_shard, prevent_writes: false) if ActiveRecord.legacy_connection_handling raise NotImplementedError, "`connecting_to` is not available with `legacy_connection_handling`." end prevent_writes = true if role == ActiveRecord.reading_role append_to_connected_to_stack(role: role, shard: shard, prevent_writes: prevent_writes, klasses: [self]) end
def connection
also be used to "borrow" the connection to do database work unrelated
Returns the connection currently associated with the class. This can
def connection retrieve_connection end
def connection_db_config
@name="primary", @config={pool: 5, timeout: 5000, database: "db/development.sqlite3", adapter: "sqlite3"}>
#
Returns the db_config object from the associated connection:
def connection_db_config connection_pool.db_config end
def connection_pool
def connection_pool connection_handler.retrieve_connection_pool(connection_specification_name, role: current_role, shard: current_shard) || raise(ConnectionNotEstablished) end
def connection_specification_name
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 connects_to(database: {}, shards: {})
end
}
shard_two: { writing: :primary_shard_two, reading: :primary_shard_replica_two }
default: { writing: :primary, reading: :primary_replica },
connects_to shards: {
self.abstract_class = true
class AnimalsModel < ApplicationRecord
also supports read replicas. Connect a model to a list of shards like this:
+connects_to+ also supports horizontal sharding. The horizontal sharding API
end
connects_to database: { writing: :primary, reading: :primary_replica }
self.abstract_class = true
class AnimalsModel < ApplicationRecord
establishes a connection to that config.
look up the config hash using the +database_key+ and finally
This will create a connection handler for switching between connections,
takes a hash consisting of a +role+ and a +database_key+.
Connects a model to the databases specified. The +database+ keyword
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
def establish_connection(config_or_env = nil)
The exceptions AdapterNotSpecified, AdapterNotFound, and +ArgumentError+
ActiveRecord::Base.establish_connection(:production)
configuration hash:
a symbol can also be given as argument, representing a key in the
is set (Rails automatically loads the contents of config/database.yml into it),
In case {ActiveRecord::Base.configurations}[rdoc-ref:Core.configurations]
)
"postgres://myuser:mypass@localhost/somedatabase"
ActiveRecord::Base.establish_connection(
Or a URL:
)
"database" => "path/to/dbfile"
"adapter" => "sqlite3",
ActiveRecord::Base.establish_connection(
Also accepts keys as strings (for parsing from YAML for example):
)
database: "path/to/dbfile"
adapter: "sqlite3",
ActiveRecord::Base.establish_connection(
Example for SQLite database:
)
database: "somedatabase"
password: "mypass",
username: "myuser",
host: "localhost",
adapter: "mysql2",
ActiveRecord::Base.establish_connection(
example for regular databases (MySQL, PostgreSQL, etc):
the :adapter key must be specified with the name of a database adapter (in lower-case)
Establishes the connection to the database. Accepts a hash as input where
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
def lookup_connection_handler(handler_key) # :nodoc:
def lookup_connection_handler(handler_key) # :nodoc: if ActiveRecord.legacy_connection_handling handler_key ||= ActiveRecord.writing_role connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new else ActiveRecord::Base.connection_handler end end
def mysql2_connection(config)
:nodoc:
def mysql2_connection(config) config = config.symbolize_keys config[:flags] ||= 0 if config[:flags].kind_of? Array config[:flags].push "FOUND_ROWS" else config[:flags] |= Mysql2::Client::FOUND_ROWS end ConnectionAdapters::Mysql2Adapter.new( ConnectionAdapters::Mysql2Adapter.new_client(config), logger, nil, config, ) end
def postgresql_connection(config)
:nodoc:
def postgresql_connection(config) conn_params = config.symbolize_keys.compact # Map ActiveRecords param names to PGs. conn_params[:user] = conn_params.delete(:username) if conn_params[:username] conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database] # Forward only valid config params to PG::Connection.connect. valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl] conn_params.slice!(*valid_conn_param_keys) ConnectionAdapters::PostgreSQLAdapter.new( ConnectionAdapters::PostgreSQLAdapter.new_client(conn_params), logger, conn_params, config, ) end
def primary_class? # :nodoc:
def primary_class? # :nodoc: self == Base || application_record_class? end
def prohibit_shard_swapping(enabled = true)
is useful in cases you're using sharding to provide per-request
nested call to connected_to or connected_to_many to swap again. This
In some cases you may want to be able to swap shards but not allow a
Prohibit swapping shards while inside of the passed block.
def prohibit_shard_swapping(enabled = true) prev_value = ActiveSupport::IsolatedExecutionState[:active_record_prohibit_shard_swapping] ActiveSupport::IsolatedExecutionState[:active_record_prohibit_shard_swapping] = enabled yield ensure ActiveSupport::IsolatedExecutionState[:active_record_prohibit_shard_swapping] = prev_value end
def remove_connection(name = nil)
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 resolve_config_for_connection(config_or_env)
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 retrieve_connection
def retrieve_connection connection_handler.retrieve_connection(connection_specification_name, role: current_role, shard: current_shard) end
def shard_swapping_prohibited?
def shard_swapping_prohibited? ActiveSupport::IsolatedExecutionState[:active_record_prohibit_shard_swapping] end
def sqlite3_connection(config)
def sqlite3_connection(config) config = config.symbolize_keys # Require database. unless config[:database] raise ArgumentError, "No database file specified. Missing argument: database" end # Allow database path relative to Rails.root, but only if the database # path is not the special path that tells sqlite to build a database only # in memory. if ":memory:" != config[:database] && !config[:database].to_s.start_with?("file:") config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root) dirname = File.dirname(config[:database]) Dir.mkdir(dirname) unless File.directory?(dirname) end db = SQLite3::Database.new( config[:database].to_s, config.merge(results_as_hash: true) ) ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config) rescue Errno::ENOENT => error if error.message.include?("No such file or directory") raise ActiveRecord::NoDatabaseError else raise end end
def swap_connection_handler(handler, &blk) # :nodoc:
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
def while_preventing_writes(enabled = true, &block)
See +READ_QUERY+ for the queries that are blocked by this
user and is meant to be a safeguard against accidental writes.
This method does not provide the same protection as a readonly
will prevent writes to the database for the duration of the block.
even if you are on a database that can write. +while_preventing_writes+
In some cases you may want to prevent writes to the database
Prevent writing to the database regardless of role.
def while_preventing_writes(enabled = true, &block) if ActiveRecord.legacy_connection_handling connection_handler.while_preventing_writes(enabled, &block) else connected_to(role: current_role, prevent_writes: enabled, &block) end end
def with_handler(handler_key, &blk)
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)
def with_role_and_shard(role, shard, prevent_writes) prevent_writes = true if role == ActiveRecord.reading_role if ActiveRecord.legacy_connection_handling with_handler(role.to_sym) do connection_handler.while_preventing_writes(prevent_writes) do append_to_connected_to_stack(shard: shard, klasses: [self]) yield end end else append_to_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