Skip to content

Commit

Permalink
Merge pull request #41258 from eileencodes/primary-class
Browse files Browse the repository at this point in the history
Expose `primary_abstract_class` public API
  • Loading branch information
eileencodes committed Feb 4, 2021
2 parents 3f96069 + 4144746 commit 5a47789
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 3 deletions.
19 changes: 19 additions & 0 deletions activerecord/CHANGELOG.md
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/connection_handling.rb
Expand Up @@ -276,7 +276,7 @@ def connection_specification_name
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:
Expand Down
13 changes: 13 additions & 0 deletions activerecord/lib/active_record/core.rb
Expand Up @@ -155,6 +155,19 @@ def self.configurations

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
Expand Down
15 changes: 15 additions & 0 deletions activerecord/lib/active_record/inheritance.rb
Expand Up @@ -164,6 +164,21 @@ def abstract_class?
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
Expand Down
Expand Up @@ -448,6 +448,7 @@ def test_application_record_prevent_writes_can_be_changed
end
end
ensure
ActiveRecord::Base.application_record_class = nil
Object.send(:remove_const, :ApplicationRecord)
ActiveRecord::Base.establish_connection :arunit
end
Expand Down
111 changes: 111 additions & 0 deletions activerecord/test/cases/primary_class_test.rb
@@ -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
14 changes: 12 additions & 2 deletions guides/source/active_record_multiple_databases.md
Expand Up @@ -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
Expand Down

0 comments on commit 5a47789

Please sign in to comment.