Support establishing connection on ActiveRecord::Model.

This is the 'top level' connection, inherited by any models that include
ActiveRecord::Model or inherit from ActiveRecord::Base.
This commit is contained in:
Jon Leighton 2011-12-28 18:07:08 +00:00
parent 93c1f11c0a
commit dae7b65453
25 changed files with 251 additions and 230 deletions

View File

@ -59,6 +59,7 @@ module ActiveRecord
autoload :Callbacks autoload :Callbacks
autoload :Core autoload :Core
autoload :CounterCache autoload :CounterCache
autoload :ConnectionHandling
autoload :DynamicMatchers autoload :DynamicMatchers
autoload :DynamicFinderMatch autoload :DynamicFinderMatch
autoload :DynamicScopeMatch autoload :DynamicScopeMatch

View File

@ -61,7 +61,7 @@ module ActiveRecord
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
end end
if active_record_super == Base if [Base, Model].include?(active_record_super)
super super
else else
# If B < A and A defines its own attribute method, then we don't want to overwrite that. # If B < A and A defines its own attribute method, then we don't want to overwrite that.

View File

@ -331,5 +331,4 @@ module ActiveRecord #:nodoc:
end end
end end
require 'active_record/connection_adapters/abstract/connection_specification'
ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy) ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy)

View File

@ -370,7 +370,7 @@ connection. For example: ActiveRecord::Base.connection.close
def retrieve_connection_pool(klass) def retrieve_connection_pool(klass)
pool = @class_to_pool[klass.name] pool = @class_to_pool[klass.name]
return pool if pool return pool if pool
return nil if ActiveRecord::Base == klass return nil if ActiveRecord::Model == klass
retrieve_connection_pool klass.active_record_super retrieve_connection_pool klass.active_record_super
end end
end end

View File

@ -1,184 +0,0 @@
require 'active_support/core_ext/module/delegation'
module ActiveRecord
module Core
class ConnectionSpecification #:nodoc:
attr_reader :config, :adapter_method
def initialize (config, adapter_method)
@config, @adapter_method = config, adapter_method
end
##
# Builds a ConnectionSpecification from user input
class Resolver # :nodoc:
attr_reader :config, :klass, :configurations
def initialize(config, configurations)
@config = config
@configurations = configurations
end
def spec
case config
when nil
raise AdapterNotSpecified unless defined?(Rails.env)
resolve_string_connection Rails.env
when Symbol, String
resolve_string_connection config.to_s
when Hash
resolve_hash_connection config
end
end
private
def resolve_string_connection(spec) # :nodoc:
hash = configurations.fetch(spec) do |k|
connection_url_to_hash(k)
end
raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash
resolve_hash_connection hash
end
def resolve_hash_connection(spec) # :nodoc:
spec = spec.symbolize_keys
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
begin
require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
rescue LoadError => e
raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace
end
adapter_method = "#{spec[:adapter]}_connection"
ConnectionSpecification.new(spec, adapter_method)
end
def connection_url_to_hash(url) # :nodoc:
config = URI.parse url
adapter = config.scheme
adapter = "postgresql" if adapter == "postgres"
spec = { :adapter => adapter,
:username => config.user,
:password => config.password,
:port => config.port,
:database => config.path.sub(%r{^/},""),
:host => config.host }
spec.reject!{ |_,value| !value }
if config.query
options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
spec.merge!(options)
end
spec
end
end
end
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work that isn't
# easily done without going straight to SQL.
def connection
self.class.connection
end
module ClassMethods
# 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)
# example for regular databases (MySQL, Postgresql, etc):
#
# ActiveRecord::Base.establish_connection(
# :adapter => "mysql",
# :host => "localhost",
# :username => "myuser",
# :password => "mypass",
# :database => "somedatabase"
# )
#
# Example for SQLite database:
#
# ActiveRecord::Base.establish_connection(
# :adapter => "sqlite",
# :database => "path/to/dbfile"
# )
#
# Also accepts keys as strings (for parsing from YAML for example):
#
# ActiveRecord::Base.establish_connection(
# "adapter" => "sqlite",
# "database" => "path/to/dbfile"
# )
#
# Or a URL:
#
# ActiveRecord::Base.establish_connection(
# "postgres://myuser:mypass@localhost/somedatabase"
# )
#
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
# may be returned on an error.
def establish_connection(spec = ENV["DATABASE_URL"])
resolver = ConnectionSpecification::Resolver.new spec, configurations
spec = resolver.spec
unless respond_to?(spec.adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
end
remove_connection
connection_handler.establish_connection name, spec
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
def connection_id
Thread.current['ActiveRecord::Base.connection_id']
end
def connection_id=(connection_id)
Thread.current['ActiveRecord::Base.connection_id'] = connection_id
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.spec.config
end
def connection_pool
connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished
end
def retrieve_connection
connection_handler.retrieve_connection(self)
end
# Returns true if Active Record is connected.
def connected?
connection_handler.connected?(self)
end
def remove_connection(klass = self)
connection_handler.remove_connection(klass)
end
def clear_active_connections!
connection_handler.clear_active_connections!
end
delegate :clear_reloadable_connections!,
:clear_all_connections!,:verify_active_connections!, :to => :connection_handler
end
end
end

View File

@ -11,6 +11,7 @@ module ActiveRecord
extend ActiveSupport::Autoload extend ActiveSupport::Autoload
autoload :Column autoload :Column
autoload :ConnectionSpecification
autoload_under 'abstract' do autoload_under 'abstract' do
autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions' autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions'
@ -26,7 +27,6 @@ module ActiveRecord
autoload :ConnectionPool autoload :ConnectionPool
autoload :ConnectionHandler, 'active_record/connection_adapters/abstract/connection_pool' autoload :ConnectionHandler, 'active_record/connection_adapters/abstract/connection_pool'
autoload :ConnectionManagement, 'active_record/connection_adapters/abstract/connection_pool' autoload :ConnectionManagement, 'active_record/connection_adapters/abstract/connection_pool'
autoload :ConnectionSpecification
autoload :QueryCache autoload :QueryCache
end end

View File

@ -0,0 +1,79 @@
module ActiveRecord
module ConnectionAdapters
class ConnectionSpecification #:nodoc:
attr_reader :config, :adapter_method
def initialize (config, adapter_method)
@config, @adapter_method = config, adapter_method
end
##
# Builds a ConnectionSpecification from user input
class Resolver # :nodoc:
attr_reader :config, :klass, :configurations
def initialize(config, configurations)
@config = config
@configurations = configurations
end
def spec
case config
when nil
raise AdapterNotSpecified unless defined?(Rails.env)
resolve_string_connection Rails.env
when Symbol, String
resolve_string_connection config.to_s
when Hash
resolve_hash_connection config
end
end
private
def resolve_string_connection(spec) # :nodoc:
hash = configurations.fetch(spec) do |k|
connection_url_to_hash(k)
end
raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash
resolve_hash_connection hash
end
def resolve_hash_connection(spec) # :nodoc:
spec = spec.symbolize_keys
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
begin
require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
rescue LoadError => e
raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace
end
adapter_method = "#{spec[:adapter]}_connection"
ConnectionSpecification.new(spec, adapter_method)
end
def connection_url_to_hash(url) # :nodoc:
config = URI.parse url
adapter = config.scheme
adapter = "postgresql" if adapter == "postgres"
spec = { :adapter => adapter,
:username => config.user,
:password => config.password,
:port => config.port,
:database => config.path.sub(%r{^/},""),
:host => config.host }
spec.reject!{ |_,value| !value }
if config.query
options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
spec.merge!(options)
end
spec
end
end
end
end
end

View File

@ -4,7 +4,7 @@ gem 'mysql2', '~> 0.3.10'
require 'mysql2' require 'mysql2'
module ActiveRecord module ActiveRecord
module Core::ClassMethods module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects. # Establishes a connection to the database that's used by all Active Record objects.
def mysql2_connection(config) def mysql2_connection(config)
config[:username] = 'root' if config[:username].nil? config[:username] = 'root' if config[:username].nil?

View File

@ -18,7 +18,7 @@ class Mysql
end end
module ActiveRecord module ActiveRecord
module Core::ClassMethods module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects. # Establishes a connection to the database that's used by all Active Record objects.
def mysql_connection(config) # :nodoc: def mysql_connection(config) # :nodoc:
config = config.symbolize_keys config = config.symbolize_keys

View File

@ -7,7 +7,7 @@ gem 'pg', '~> 0.11'
require 'pg' require 'pg'
module ActiveRecord module ActiveRecord
module Core::ClassMethods module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects # Establishes a connection to the database that's used by all Active Record objects
def postgresql_connection(config) # :nodoc: def postgresql_connection(config) # :nodoc:
config = config.symbolize_keys config = config.symbolize_keys

View File

@ -4,7 +4,7 @@ gem 'sqlite3', '~> 1.3.5'
require 'sqlite3' require 'sqlite3'
module ActiveRecord module ActiveRecord
module Core::ClassMethods module ConnectionHandling
# sqlite3 adapter reuses sqlite_connection. # sqlite3 adapter reuses sqlite_connection.
def sqlite3_connection(config) # :nodoc: def sqlite3_connection(config) # :nodoc:
# Require database. # Require database.

View File

@ -0,0 +1,100 @@
require 'active_support/core_ext/module/delegation'
module ActiveRecord
module ConnectionHandling
# 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)
# example for regular databases (MySQL, Postgresql, etc):
#
# ActiveRecord::Base.establish_connection(
# :adapter => "mysql",
# :host => "localhost",
# :username => "myuser",
# :password => "mypass",
# :database => "somedatabase"
# )
#
# Example for SQLite database:
#
# ActiveRecord::Base.establish_connection(
# :adapter => "sqlite",
# :database => "path/to/dbfile"
# )
#
# Also accepts keys as strings (for parsing from YAML for example):
#
# ActiveRecord::Base.establish_connection(
# "adapter" => "sqlite",
# "database" => "path/to/dbfile"
# )
#
# Or a URL:
#
# ActiveRecord::Base.establish_connection(
# "postgres://myuser:mypass@localhost/somedatabase"
# )
#
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
# may be returned on an error.
def establish_connection(spec = ENV["DATABASE_URL"])
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations
spec = resolver.spec
unless respond_to?(spec.adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
end
remove_connection
connection_handler.establish_connection name, spec
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
def connection_id
Thread.current['ActiveRecord::Base.connection_id']
end
def connection_id=(connection_id)
Thread.current['ActiveRecord::Base.connection_id'] = connection_id
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.spec.config
end
def connection_pool
connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished
end
def retrieve_connection
connection_handler.retrieve_connection(self)
end
# Returns true if Active Record is connected.
def connected?
connection_handler.connected?(self)
end
def remove_connection(klass = self)
connection_handler.remove_connection(klass)
end
def clear_active_connections!
connection_handler.clear_active_connections!
end
delegate :clear_reloadable_connections!,
:clear_all_connections!,:verify_active_connections!, :to => :connection_handler
end
end

View File

@ -119,13 +119,7 @@ module ActiveRecord
end end
def arel_engine def arel_engine
@arel_engine ||= begin @arel_engine ||= connection_handler.connection_pools[name] ? self : active_record_super.arel_engine
if self == ActiveRecord::Base
ActiveRecord::Base
else
connection_handler.connection_pools[name] ? self : active_record_super.arel_engine
end
end
end end
private private
@ -304,6 +298,13 @@ module ActiveRecord
@readonly = true @readonly = true
end end
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work that isn't
# easily done without going straight to SQL.
def connection
self.class.connection
end
# Returns the contents of the record as a nicely formatted string. # Returns the contents of the record as a nicely formatted string.
def inspect def inspect
inspection = if @attributes inspection = if @attributes

View File

@ -17,8 +17,10 @@ module ActiveRecord
if sup.abstract_class? if sup.abstract_class?
sup.descends_from_active_record? sup.descends_from_active_record?
elsif self == Base
false
else else
sup == Base || !columns_hash.include?(inheritance_column) [Base, Model].include?(sup) || !columns_hash.include?(inheritance_column)
end end
end end
@ -81,18 +83,12 @@ module ActiveRecord
instance instance
end end
# If this class includes ActiveRecord::Model then it won't have a # For internal use.
# superclass. So this provides a way to get to the 'root' (ActiveRecord::Base),
# through inheritance hierarchy, ending in Base, whether or not that is
# actually an ancestor of the class.
# #
# Mainly for internal use. # If this class includes ActiveRecord::Model then it won't have a
# superclass. So this provides a way to get to the 'root' (ActiveRecord::Model).
def active_record_super #:nodoc: def active_record_super #:nodoc:
if self == Base || superclass && superclass < Model superclass < Model ? superclass : Model
superclass
else
Base
end
end end
protected protected
@ -105,7 +101,7 @@ module ActiveRecord
end end
sup = klass.active_record_super sup = klass.active_record_super
if klass == Base || sup == Base || sup.abstract_class? if [Base, Model].include?(klass) || [Base, Model].include?(sup) || sup.abstract_class?
klass klass
else else
class_of_active_record_descendant(sup) class_of_active_record_descendant(sup)

View File

@ -22,6 +22,7 @@ module ActiveRecord
include DynamicMatchers include DynamicMatchers
include CounterCache include CounterCache
include Explain include Explain
include ConnectionHandling
end end
def self.included(base) def self.included(base)
@ -40,6 +41,7 @@ module ActiveRecord
extend ActiveModel::AttributeMethods::ClassMethods extend ActiveModel::AttributeMethods::ClassMethods
extend Callbacks::Register extend Callbacks::Register
extend Explain extend Explain
extend ConnectionHandling
def self.extend(*modules) def self.extend(*modules)
ClassMethods.send(:include, *modules) ClassMethods.send(:include, *modules)
@ -65,6 +67,20 @@ module ActiveRecord
include Aggregations, Transactions, Reflection, Serialization, Store include Aggregations, Transactions, Reflection, Serialization, Store
include Core include Core
class << self
def arel_engine
self
end
def abstract_class?
false
end
def inheritance_column
'type'
end
end
module DeprecationProxy #:nodoc: module DeprecationProxy #:nodoc:
class << self class << self
instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_eval$/ } instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_eval$/ }

View File

@ -143,11 +143,7 @@ module ActiveRecord
# The name of the column containing the object's class when Single Table Inheritance is used # The name of the column containing the object's class when Single Table Inheritance is used
def inheritance_column def inheritance_column
if self == Base (@inheritance_column ||= nil) || active_record_super.inheritance_column
'type'
else
(@inheritance_column ||= nil) || active_record_super.inheritance_column
end
end end
# Sets the value of inheritance_column # Sets the value of inheritance_column

View File

@ -23,6 +23,7 @@ require 'models/edge'
require 'models/joke' require 'models/joke'
require 'models/bulb' require 'models/bulb'
require 'models/bird' require 'models/bird'
require 'models/teapot'
require 'rexml/document' require 'rexml/document'
require 'active_support/core_ext/exception' require 'active_support/core_ext/exception'
require 'bcrypt' require 'bcrypt'
@ -1634,10 +1635,7 @@ class BasicsTest < ActiveRecord::TestCase
end end
def test_descends_from_active_record def test_descends_from_active_record
# Tries to call Object.abstract_class? assert !ActiveRecord::Base.descends_from_active_record?
assert_raise(NoMethodError) do
ActiveRecord::Base.descends_from_active_record?
end
# Abstract subclass of AR::Base. # Abstract subclass of AR::Base.
assert LoosePerson.descends_from_active_record? assert LoosePerson.descends_from_active_record?
@ -1660,6 +1658,10 @@ class BasicsTest < ActiveRecord::TestCase
# Concrete subclasses an abstract class which has a type column. # Concrete subclasses an abstract class which has a type column.
assert !SubStiPost.descends_from_active_record? assert !SubStiPost.descends_from_active_record?
assert Teapot.descends_from_active_record?
assert !OtherTeapot.descends_from_active_record?
assert CoolTeapot.descends_from_active_record?
end end
def test_find_on_abstract_base_class_doesnt_use_type_condition def test_find_on_abstract_base_class_doesnt_use_type_condition
@ -1893,4 +1895,13 @@ class BasicsTest < ActiveRecord::TestCase
Bird.stubs(:scoped).returns(mock(:uniq => scope)) Bird.stubs(:scoped).returns(mock(:uniq => scope))
assert_equal scope, Bird.uniq assert_equal scope, Bird.uniq
end end
def test_active_record_super
assert_equal ActiveRecord::Model, ActiveRecord::Base.active_record_super
assert_equal ActiveRecord::Base, Topic.active_record_super
assert_equal Topic, ImportantTopic.active_record_super
assert_equal ActiveRecord::Model, Teapot.active_record_super
assert_equal Teapot, OtherTeapot.active_record_super
assert_equal ActiveRecord::Model, CoolTeapot.active_record_super
end
end end

View File

@ -35,7 +35,7 @@ module ActiveRecord
end end
def test_close def test_close
pool = ConnectionPool.new(Base::ConnectionSpecification.new({}, nil)) pool = ConnectionPool.new(ConnectionSpecification.new({}, nil))
pool.connections << adapter pool.connections << adapter
adapter.pool = pool adapter.pool = pool

View File

@ -1,7 +1,7 @@
require "cases/helper" require "cases/helper"
module ActiveRecord module ActiveRecord
module Core module ConnectionAdapters
class ConnectionSpecification class ConnectionSpecification
class ResolverTest < ActiveRecord::TestCase class ResolverTest < ActiveRecord::TestCase
def resolve(spec) def resolve(spec)

View File

@ -56,10 +56,13 @@ class InclusionUnitTest < ActiveRecord::TestCase
def test_establish_connection def test_establish_connection
assert @klass.respond_to?(:establish_connection) assert @klass.respond_to?(:establish_connection)
assert ActiveRecord::Model.respond_to?(:establish_connection)
end end
def test_adapter_connection def test_adapter_connection
assert @klass.respond_to?("#{ActiveRecord::Base.connection_config[:adapter]}_connection") name = "#{ActiveRecord::Base.connection_config[:adapter]}_connection"
assert @klass.respond_to?(name)
assert ActiveRecord::Model.respond_to?(name)
end end
def test_connection_handler def test_connection_handler

View File

@ -65,7 +65,6 @@ class InheritanceTest < ActiveRecord::TestCase
end end
def test_company_descends_from_active_record def test_company_descends_from_active_record
assert_raise(NoMethodError) { ActiveRecord::Base.descends_from_active_record? }
assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base' assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base'
assert Company.descends_from_active_record?, 'Company should descend from ActiveRecord::Base' assert Company.descends_from_active_record?, 'Company should descend from ActiveRecord::Base'
assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base' assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base'

View File

@ -7,13 +7,13 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
self.use_transactional_fixtures = false self.use_transactional_fixtures = false
def setup def setup
@underlying = ActiveRecord::Base.connection @underlying = ActiveRecord::Model.connection
@specification = ActiveRecord::Base.remove_connection @specification = ActiveRecord::Model.remove_connection
end end
def teardown def teardown
@underlying = nil @underlying = nil
ActiveRecord::Base.establish_connection(@specification) ActiveRecord::Model.establish_connection(@specification)
load_schema if in_memory_db? load_schema if in_memory_db?
end end

View File

@ -12,6 +12,9 @@ class Teapot
include ActiveRecord::Model include ActiveRecord::Model
end end
class OtherTeapot < Teapot
end
class OMFGIMATEAPOT class OMFGIMATEAPOT
def aaahhh def aaahhh
"mmm" "mmm"

View File

@ -598,6 +598,7 @@ ActiveRecord::Schema.define do
create_table :teapots, :force => true do |t| create_table :teapots, :force => true do |t|
t.string :name t.string :name
t.string :type
t.timestamps t.timestamps
end end

View File

@ -12,9 +12,9 @@ module ARTest
def self.connect def self.connect
puts "Using #{connection_name} with Identity Map #{ActiveRecord::IdentityMap.enabled? ? 'on' : 'off'}" puts "Using #{connection_name} with Identity Map #{ActiveRecord::IdentityMap.enabled? ? 'on' : 'off'}"
ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log") ActiveRecord::Model.logger = ActiveSupport::Logger.new("debug.log")
ActiveRecord::Base.configurations = connection_config ActiveRecord::Model.configurations = connection_config
ActiveRecord::Base.establish_connection 'arunit' ActiveRecord::Model.establish_connection 'arunit'
Course.establish_connection 'arunit2' Course.establish_connection 'arunit2'
end end
end end