connection_handling.rb 8.8 KB
Newer Older
1 2
# frozen_string_literal: true

3 4
module ActiveRecord
  module ConnectionHandling
5
    RAILS_ENV   = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence }
6 7
    DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" }

8 9
    # Establishes the connection to the database. Accepts a hash as input where
    # the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case)
10
    # example for regular databases (MySQL, PostgreSQL, etc):
11 12
    #
    #   ActiveRecord::Base.establish_connection(
R
Ryuta Kamizono 已提交
13
    #     adapter:  "mysql2",
A
AvnerCohen 已提交
14 15 16 17
    #     host:     "localhost",
    #     username: "myuser",
    #     password: "mypass",
    #     database: "somedatabase"
18 19 20 21 22
    #   )
    #
    # Example for SQLite database:
    #
    #   ActiveRecord::Base.establish_connection(
J
Julian Simioni 已提交
23
    #     adapter:  "sqlite3",
24
    #     database: "path/to/dbfile"
25 26 27 28 29
    #   )
    #
    # Also accepts keys as strings (for parsing from YAML for example):
    #
    #   ActiveRecord::Base.establish_connection(
J
Julian Simioni 已提交
30
    #     "adapter"  => "sqlite3",
31
    #     "database" => "path/to/dbfile"
32 33 34 35 36 37 38 39
    #   )
    #
    # Or a URL:
    #
    #   ActiveRecord::Base.establish_connection(
    #     "postgres://myuser:mypass@localhost/somedatabase"
    #   )
    #
40 41
    # In case {ActiveRecord::Base.configurations}[rdoc-ref:Core.configurations]
    # is set (Rails automatically loads the contents of config/database.yml into it),
42 43 44 45 46
    # a symbol can also be given as argument, representing a key in the
    # configuration hash:
    #
    #   ActiveRecord::Base.establish_connection(:production)
    #
47
    # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+
48
    # may be returned on an error.
49
    def establish_connection(config_or_env = nil)
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
      config_hash = resolve_config_for_connection(config_or_env)
      connection_handler.establish_connection(config_hash)
    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
    #
    # Returns an array of established connections.
    def connects_to(database: {})
      connections = []

      database.each do |role, database_key|
        config_hash = resolve_config_for_connection(database_key)
        handler = lookup_connection_handler(role.to_sym)

        connections << handler.establish_connection(config_hash)
      end

      connections
    end

    # Connects to a database or role (ex writing, reading, or another
    # custom role) for the duration of the block.
    #
    # If a role is passed, Active Record will look up the connection
    # based on the requested role:
    #
    #   ActiveRecord::Base.connected_to(role: :writing) do
    #     Dog.create! # creates dog using dog connection
    #   end
    #
    #   ActiveRecord::Base.connected_to(role: :reading) do
    #     Dog.create! # throws exception because we're on a replica
    #   end
    #
    #   ActiveRecord::Base.connected_to(role: :unknown_ode) do
    #     # raises exception due to non-existent role
    #   end
    #
    # For cases where you may want to connect to a database outside of the model,
    # you can use +connected_to+ with a +database+ argument. The +database+ argument
    # expects a symbol that corresponds to the database key in your config.
    #
    # This will connect to a new database for the queries inside the block.
    #
    #   ActiveRecord::Base.connected_to(database: :animals_slow_replica) do
    #     Dog.run_a_long_query # runs a long query while connected to the +animals_slow_replica+
    #   end
    def connected_to(database: nil, role: nil, &blk)
      if database && role
110
        raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments."
111
      elsif database
112 113 114 115 116 117 118 119 120
        if database.is_a?(Hash)
          role, database = database.first
          role = role.to_sym
        else
          role = database.to_sym
        end

        config_hash = resolve_config_for_connection(database)
        handler = lookup_connection_handler(role)
121

122 123 124
        with_handler(role) do
          handler.establish_connection(config_hash)
          yield
125 126 127 128 129 130 131 132
        end
      elsif role
        with_handler(role.to_sym, &blk)
      else
        raise ArgumentError, "must provide a `database` or a `role`."
      end
    end

J
John Hawthorn 已提交
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
    # 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:)
      current_role == role.to_sym
    end

    # Returns the symbol representing the current connected role.
    #
    #   ActiveRecord::Base.connected_to(role: :writing) do
    #     ActiveRecord::Base.current_role #=> :writing
    #   end
    #
    #   ActiveRecord::Base.connected_to(role: :reading) do
    #     ActiveRecord::Base.current_role #=> :reading
    #   end
    def current_role
      connection_handlers.key(connection_handler)
    end

156 157 158 159 160 161 162 163 164 165
    def lookup_connection_handler(handler_key) # :nodoc:
      connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new
    end

    def with_handler(handler_key, &blk) # :nodoc:
      handler = lookup_connection_handler(handler_key)
      swap_connection_handler(handler, &blk)
    end

    def resolve_config_for_connection(config_or_env) # :nodoc:
J
Jon Moss 已提交
166
      raise "Anonymous class is not allowed." unless name
A
Arthur Neves 已提交
167

168 169 170
      config_or_env ||= DEFAULT_ENV.call.to_sym
      pool_name = self == Base ? "primary" : name
      self.connection_specification_name = pool_name
171 172

      resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations)
173 174
      config_hash = resolver.resolve(config_or_env, pool_name).symbolize_keys
      config_hash[:name] = pool_name
175

176
      config_hash
177 178
    end

179 180 181 182 183 184 185 186 187
    # Clears the query cache for all connections associated with the current thread.
    def clear_query_caches_for_current_thread
      ActiveRecord::Base.connection_handlers.each_value do |handler|
        handler.connection_pool_list.each do |pool|
          pool.connection.clear_query_cache if pool.active_connection?
        end
      end
    end

188 189 190 191 192 193 194
    # 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

195
    attr_writer :connection_specification_name
A
Arthur Neves 已提交
196

J
Jon Moss 已提交
197
    # Return the specification name from the current class or its parent.
198
    def connection_specification_name
199
      if !defined?(@connection_specification_name) || @connection_specification_name.nil?
200
        return self == Base ? "primary" : superclass.connection_specification_name
A
Arthur Neves 已提交
201
      end
202
      @connection_specification_name
A
Arthur Neves 已提交
203 204
    end

205 206 207
    # Returns the configuration of the associated connection as a hash:
    #
    #  ActiveRecord::Base.connection_config
A
AvnerCohen 已提交
208
    #  # => {pool: 5, timeout: 5000, database: "db/development.sqlite3", adapter: "sqlite3"}
209 210 211 212 213 214 215
    #
    # Please use only for reading.
    def connection_config
      connection_pool.spec.config
    end

    def connection_pool
216
      connection_handler.retrieve_connection_pool(connection_specification_name) || raise(ConnectionNotEstablished)
217 218 219
    end

    def retrieve_connection
220
      connection_handler.retrieve_connection(connection_specification_name)
221 222
    end

223
    # Returns +true+ if Active Record is connected.
224
    def connected?
225
      connection_handler.connected?(connection_specification_name)
226 227
    end

228 229
    def remove_connection(name = nil)
      name ||= @connection_specification_name if defined?(@connection_specification_name)
230
      # if removing a connection that has a pool, we reset the
231 232 233 234 235 236
      # connection_specification_name so it will use the parent
      # pool.
      if connection_handler.retrieve_connection_pool(name)
        self.connection_specification_name = nil
      end

237
      connection_handler.remove_connection(name)
238 239
    end

J
Jon Leighton 已提交
240 241 242 243
    def clear_cache! # :nodoc:
      connection.schema_cache.clear!
    end

244
    delegate :clear_active_connections!, :clear_reloadable_connections!,
M
Matthew Draper 已提交
245
      :clear_all_connections!, :flush_idle_connections!, to: :connection_handler
246 247 248 249 250 251 252 253 254

    private

      def swap_connection_handler(handler, &blk) # :nodoc:
        old_handler, ActiveRecord::Base.connection_handler = ActiveRecord::Base.connection_handler, handler
        yield
      ensure
        ActiveRecord::Base.connection_handler = old_handler
      end
255 256
  end
end