Expose `primary_abstract_class` public API

Previously Rails was treating `ApplicationRecord` as special in the
`primary_class?` check. The reason we need to treat it differtently than
other connection classes is that `ActiveRecord::Base` will establish a
connection to the primary database on boot. The established connection
is to your primary database, or the first database defined in your
configuration. We need to do this so that 2 connections aren't opened to
the same database since `ActiveRecord::Base` and `AppliationRecord`
are different classes, on connection the connection_speicification_name
would be different.

However, there is no guarantee that an application is using
`ApplicationRecord` as it's primary abstract class. This exposes a
public method for setting a class to a `primary_abstract_class` like
this:

```
class PrimaryApplicationRecord < ActiveRecord::Base
  self.primary_abstract_class
end
```

Calling `primary_abstract_class` will automatically set
`self.abstract_class = true`. This change is backwards compatible
because we weren't supporting multiple application records previously,
and if you had an `ApplicationRecord` we assumed that was the primary
class. This change continues to assume that `ApplicationRecord` is your
primary class. You only need to set `primary_abstract_class` if your
application record is not ApplicationRecord and you're using multiple
databases.

Co-authored-by: John Crepezzi <john.crepezzi@gmail.com>
This commit is contained in:
eileencodes 2021-01-25 10:52:56 -05:00
parent bcc6c4b2ba
commit 4144746d33
No known key found for this signature in database
GPG Key ID: BA5C575120BBE8DF
7 changed files with 172 additions and 3 deletions

View File

@ -1,3 +1,22 @@
* Expose a way for applications to set a `primary_abstract_class`
Multiple database applications that use a primary abstract class that is not
named `ApplicationRecord` can now set a specific class to be the `primary_abstract_class`.
```ruby
class PrimaryApplicationRecord
self.primary_abstract_class
end
```
When an application boots it automatically connects to the primary or first database in the
database configuration file. In a multiple database application that then call `connects_to`
needs to know that the default connection is the same as the `ApplicationRecord` connection.
However some applications have a differently named `ApplicationRecord`. This prevents Active
Record from opening duplicate connections to the same database.
*Eileen M. Uchitelle*, *John Crepezzi*
* Support hash config for `structure_dump_flags` and `structure_load_flags` flags
Now that Active Record supports multiple databases configuration
we need a way to pass specific flags for dump/load databases since

View File

@ -276,7 +276,7 @@ module ActiveRecord
end
def primary_class? # :nodoc:
self == Base || defined?(ApplicationRecord) && self == ApplicationRecord
self == Base || application_record_class?
end
# Returns the configuration of the associated connection as a hash:

View File

@ -155,6 +155,19 @@ module ActiveRecord
mattr_accessor :legacy_connection_handling, instance_writer: false, default: true
mattr_accessor :application_record_class, instance_accessor: false, default: nil
def self.application_record_class? # :nodoc:
if Base.application_record_class
self == Base.application_record_class
else
if defined?(ApplicationRecord) && self == ApplicationRecord
Base.application_record_class = self
true
end
end
end
self.filter_attributes = []
def self.connection_handler

View File

@ -164,6 +164,21 @@ module ActiveRecord
defined?(@abstract_class) && @abstract_class == true
end
# Sets the application record class for Active Record
#
# This is useful if your application uses a different class than
# ApplicationRecord for your primary abstract class. This class
# will share a database connection with Active Record. It is the class
# that connects to your primary database.
def primary_abstract_class
if Base.application_record_class && Base.application_record_class != self
raise ArgumentError, "The `primary_abstract_class` is already set to #{Base.application_record_class}. There can only be one `primary_abstract_class` in an application."
end
self.abstract_class = true
Base.application_record_class = self
end
# Returns the value to be stored in the inheritance column for STI.
def sti_name
store_full_sti_class && store_full_class_name ? name : name.demodulize

View File

@ -448,6 +448,7 @@ module ActiveRecord
end
end
ensure
ActiveRecord::Base.application_record_class = nil
Object.send(:remove_const, :ApplicationRecord)
ActiveRecord::Base.establish_connection :arunit
end

View File

@ -0,0 +1,111 @@
# frozen_string_literal: true
require "cases/helper"
class PrimaryClassTest < ActiveRecord::TestCase
self.use_transactional_tests = false
def teardown
clean_up_connection_handler
end
class PrimaryAppRecord < ActiveRecord::Base
end
class AnotherAppRecord < PrimaryAppRecord
self.abstract_class = true
end
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
def test_application_record_is_used_if_no_primary_class_is_set
Object.const_set(:ApplicationRecord, ApplicationRecord)
assert_predicate ApplicationRecord, :primary_class?
assert_predicate ApplicationRecord, :application_record_class?
assert_predicate ApplicationRecord, :abstract_class?
ensure
ActiveRecord::Base.application_record_class = nil
Object.send(:remove_const, :ApplicationRecord)
end
def test_primary_class_and_primary_abstract_class_behavior
PrimaryClassTest::PrimaryAppRecord.primary_abstract_class
assert_predicate PrimaryClassTest::PrimaryAppRecord, :primary_class?
assert_predicate PrimaryClassTest::PrimaryAppRecord, :application_record_class?
assert_predicate PrimaryClassTest::PrimaryAppRecord, :abstract_class?
assert_not_predicate AnotherAppRecord, :primary_class?
assert_not_predicate AnotherAppRecord, :application_record_class?
assert_predicate AnotherAppRecord, :abstract_class?
assert_predicate ActiveRecord::Base, :primary_class?
assert_not_predicate ActiveRecord::Base, :application_record_class?
assert_not_predicate ActiveRecord::Base, :abstract_class?
ensure
ActiveRecord::Base.application_record_class = nil
end
def test_primary_abstract_class_cannot_be_reset
PrimaryClassTest::PrimaryAppRecord.primary_abstract_class
assert_raises do
PrimaryClassTest::AnotherAppRecord.primary_abstract_class
end
ensure
ActiveRecord::Base.application_record_class = nil
end
def test_primary_abstract_class_is_used_over_application_record_if_set
PrimaryClassTest::PrimaryAppRecord.primary_abstract_class
Object.const_set(:ApplicationRecord, ApplicationRecord)
assert_predicate PrimaryClassTest::PrimaryAppRecord, :primary_class?
assert_predicate PrimaryClassTest::PrimaryAppRecord, :application_record_class?
assert_predicate PrimaryClassTest::PrimaryAppRecord, :abstract_class?
assert_not_predicate ApplicationRecord, :primary_class?
assert_not_predicate ApplicationRecord, :application_record_class?
assert_predicate ApplicationRecord, :abstract_class?
assert_predicate ActiveRecord::Base, :primary_class?
assert_not_predicate ActiveRecord::Base, :application_record_class?
assert_not_predicate ActiveRecord::Base, :abstract_class?
ensure
ActiveRecord::Base.application_record_class = nil
Object.send(:remove_const, :ApplicationRecord)
end
unless in_memory_db?
def test_application_record_shares_a_connection_with_active_record_by_default
Object.const_set(:ApplicationRecord, ApplicationRecord)
ApplicationRecord.connects_to(database: { writing: :arunit, reading: :arunit })
assert_predicate ApplicationRecord, :primary_class?
assert_predicate ApplicationRecord, :application_record_class?
assert_equal ActiveRecord::Base.connection, ApplicationRecord.connection
ensure
ActiveRecord::Base.application_record_class = nil
Object.send(:remove_const, :ApplicationRecord)
ActiveRecord::Base.establish_connection :arunit
end
def test_application_record_shares_a_connection_with_the_primary_abstract_class_if_set
PrimaryClassTest::PrimaryAppRecord.primary_abstract_class
PrimaryClassTest::PrimaryAppRecord.connects_to(database: { writing: :arunit, reading: :arunit })
assert_predicate PrimaryClassTest::PrimaryAppRecord, :primary_class?
assert_predicate PrimaryClassTest::PrimaryAppRecord, :application_record_class?
assert_predicate PrimaryClassTest::PrimaryAppRecord, :abstract_class?
assert_equal ActiveRecord::Base.connection, PrimaryClassTest::PrimaryAppRecord.connection
ensure
ActiveRecord::Base.application_record_class = nil
ActiveRecord::Base.establish_connection :arunit
end
end
end

View File

@ -123,8 +123,18 @@ class ApplicationRecord < ActiveRecord::Base
end
```
Classes that connect to primary/primary_replica can inherit from `ApplicationRecord` like
standard Rails applications:
If you use a differently named class for your application record you can need to
set `primary_abstract_class` instead so Rails knowns which class `ActiveRecord::Base`
should share a connection with.
```
class PrimaryApplicationRecord < ActiveRecord::Base
self.primary_abstract_class
end
```
Classes that connect to primary/primary_replica can inherit from your primary abstract
class like standard Rails applications:
```ruby
class Person < ApplicationRecord