connection_handling.rb 12.2 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)
E
eileencodes 已提交
50
      config_or_env ||= DEFAULT_ENV.call.to_sym
E
eileencodes 已提交
51
      db_config, owner_name = resolve_config_for_connection(config_or_env)
52
      connection_handler.establish_connection(db_config, owner_name: owner_name, shard: current_shard)
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
    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
    #
68 69 70 71 72 73 74 75 76 77 78 79 80 81
    # +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: {})
82 83
      raise NotImplementedError, "connects_to can only be called on ActiveRecord::Base or abstract classes" unless self == Base || abstract_class?

84 85 86 87
      if database.present? && shards.present?
        raise ArgumentError, "connects_to can only accept a `database` or `shards` argument, but not both arguments."
      end

88 89 90
      connections = []

      database.each do |role, database_key|
E
eileencodes 已提交
91
        db_config, owner_name = resolve_config_for_connection(database_key)
92 93
        handler = lookup_connection_handler(role.to_sym)

E
eileencodes 已提交
94
        connections << handler.establish_connection(db_config, owner_name: owner_name)
95 96
      end

E
eileencodes 已提交
97
      shards.each do |shard, database_keys|
98
        database_keys.each do |role, database_key|
E
eileencodes 已提交
99
          db_config, owner_name = resolve_config_for_connection(database_key)
100 101
          handler = lookup_connection_handler(role.to_sym)

102
          connections << handler.establish_connection(db_config, owner_name: owner_name, shard: shard.to_sym)
103 104 105
        end
      end

106 107 108
      connections
    end

109 110 111
    # 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.
112
    #
113 114 115
    # 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:
116 117
    #
    #   ActiveRecord::Base.connected_to(role: :writing) do
118
    #     Dog.create! # creates dog using dog writing connection
119 120 121 122 123 124
    #   end
    #
    #   ActiveRecord::Base.connected_to(role: :reading) do
    #     Dog.create! # throws exception because we're on a replica
    #   end
    #
125 126 127 128 129 130
    # If only a shard is passed, Active Record will look up the shard on the
    # current role. If a non-existent shard is passed, an
    # `ActiveRecord::ConnectionNotEstablished` error will be raised.
    #
    #   ActiveRecord::Base.connected_to(shard: :default) do
    #     # Dog.create! # creates dog in shard with the default key
131
    #   end
132 133 134 135 136 137 138 139 140 141
    #
    # If 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.create! # would raise as we're on a readonly connection
    #   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)
142 143
      raise NotImplementedError, "connected_to can only be called on ActiveRecord::Base" unless self == Base

144 145 146 147
      if database
        ActiveSupport::Deprecation.warn("The database key in `connected_to` is deprecated. It will be removed in Rails 6.2.0 without replacement.")
      end

148 149
      if database && (role || shard)
        raise ArgumentError, "`connected_to` cannot accept a `database` argument with any other arguments."
150
      elsif database
151 152 153 154 155
        if database.is_a?(Hash)
          role, database = database.first
          role = role.to_sym
        end

E
eileencodes 已提交
156
        db_config, owner_name = resolve_config_for_connection(database)
157
        handler = lookup_connection_handler(role)
158

E
eileencodes 已提交
159
        handler.establish_connection(db_config, owner_name: owner_name)
160 161

        with_handler(role, &blk)
162
      elsif shard
163 164 165 166 167
        unless role
          raise ArgumentError, "`connected_to` cannot accept a `shard` argument without a `role`."
        end

        with_shard(shard, role, prevent_writes, &blk)
168
      elsif role
169
        with_role(role, prevent_writes, &blk)
170
      else
171
        raise ArgumentError, "must provide a `shard` and/or `role`."
172 173 174
      end
    end

J
John Hawthorn 已提交
175 176 177 178 179 180
    # 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
E
eileencodes 已提交
181 182
    def connected_to?(role:, shard: ActiveRecord::Base.default_shard)
      current_role == role.to_sym && current_shard == shard.to_sym
J
John Hawthorn 已提交
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
    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

198
    def lookup_connection_handler(handler_key) # :nodoc:
199
      handler_key ||= ActiveRecord::Base.writing_role
200 201 202
      connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new
    end

203 204 205 206 207 208 209 210 211
    # 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

212 213 214 215 216 217 218
    # 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

219
    attr_writer :connection_specification_name
A
Arthur Neves 已提交
220

221
    # Return the connection specification name from the current class or its parent.
222
    def connection_specification_name
223
      if !defined?(@connection_specification_name) || @connection_specification_name.nil?
224
        return self == Base ? Base.name : superclass.connection_specification_name
A
Arthur Neves 已提交
225
      end
226
      @connection_specification_name
A
Arthur Neves 已提交
227 228
    end

229 230 231 232
    def primary_class? # :nodoc:
      self == Base || defined?(ApplicationRecord) && self == ApplicationRecord
    end

233 234 235
    # Returns the configuration of the associated connection as a hash:
    #
    #  ActiveRecord::Base.connection_config
A
AvnerCohen 已提交
236
    #  # => {pool: 5, timeout: 5000, database: "db/development.sqlite3", adapter: "sqlite3"}
237 238 239
    #
    # Please use only for reading.
    def connection_config
240
      connection_pool.db_config.configuration_hash
241
    end
J
John Crepezzi 已提交
242 243 244 245 246 247
    deprecate connection_config: "Use connection_db_config instead"

    # Returns the db_config object from the associated connection:
    #
    #  ActiveRecord::Base.connection_db_config
    #    #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development",
248
    #      @name="primary", @config={pool: 5, timeout: 5000, database: "db/development.sqlite3", adapter: "sqlite3"}>
J
John Crepezzi 已提交
249 250 251 252 253
    #
    # Use only for reading.
    def connection_db_config
      connection_pool.db_config
    end
254 255

    def connection_pool
256
      connection_handler.retrieve_connection_pool(connection_specification_name, shard: current_shard) || raise(ConnectionNotEstablished)
257 258 259
    end

    def retrieve_connection
260
      connection_handler.retrieve_connection(connection_specification_name, shard: current_shard)
261 262
    end

263
    # Returns +true+ if Active Record is connected.
264
    def connected?
265
      connection_handler.connected?(connection_specification_name, shard: current_shard)
266 267
    end

268 269
    def remove_connection(name = nil)
      name ||= @connection_specification_name if defined?(@connection_specification_name)
270
      # if removing a connection that has a pool, we reset the
271 272
      # connection_specification_name so it will use the parent
      # pool.
273
      if connection_handler.retrieve_connection_pool(name, shard: current_shard)
274 275 276
        self.connection_specification_name = nil
      end

277
      connection_handler.remove_connection_pool(name, shard: current_shard)
278 279
    end

J
Jon Leighton 已提交
280 281 282 283
    def clear_cache! # :nodoc:
      connection.schema_cache.clear!
    end

284
    delegate :clear_active_connections!, :clear_reloadable_connections!,
M
Matthew Draper 已提交
285
      :clear_all_connections!, :flush_idle_connections!, to: :connection_handler
286 287

    private
288 289 290
      def resolve_config_for_connection(config_or_env)
        raise "Anonymous class is not allowed." unless name

E
eileencodes 已提交
291 292
        owner_name = primary_class? ? Base.name : name
        self.connection_specification_name = owner_name
293

E
eileencodes 已提交
294 295
        db_config = Base.configurations.resolve(config_or_env)
        [db_config, owner_name]
296 297
      end

298 299 300 301 302 303 304 305 306 307 308 309 310
      def with_handler(handler_key, &blk)
        handler = lookup_connection_handler(handler_key)
        swap_connection_handler(handler, &blk)
      end

      def with_role(role, prevent_writes, &blk)
        prevent_writes = true if role == reading_role

        with_handler(role.to_sym) do
          connection_handler.while_preventing_writes(prevent_writes, &blk)
        end
      end

E
eileencodes 已提交
311 312
      def with_shard(shard, role, prevent_writes)
        old_shard = current_shard
313 314

        with_role(role, prevent_writes) do
E
eileencodes 已提交
315
          self.current_shard = shard
316 317 318
          yield
        end
      ensure
E
eileencodes 已提交
319
        self.current_shard = old_shard
320 321
      end

322 323
      def swap_connection_handler(handler, &blk) # :nodoc:
        old_handler, ActiveRecord::Base.connection_handler = ActiveRecord::Base.connection_handler, handler
324 325 326
        return_value = yield
        return_value.load if return_value.is_a? ActiveRecord::Relation
        return_value
327 328 329
      ensure
        ActiveRecord::Base.connection_handler = old_handler
      end
330 331
  end
end