1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/activerecord
eileencodes a2a525bbb6 Add an intermediary called RoleManager to manage connections
This PR is an alternate solution to #37388. While there are benefits
to merging #37388 it changes the public API and swaps around existing
concepts for how connection management works. The changes are
backwards-incompatible and pretty major. This will have a negative impact
on gems and applications relying on how conn management currently works.

**Background:**

Shopify and other applications need sharding but Rails has
made it impossible to do this because a handler can only hold one
connection pool per class. Sharded apps need to hold multiple
connections per handler per class.

This PR aims to solve only that problem.

**What this PR does:**

In this PR we've added a `RoleManager` class that can hold multiple
`Roles`. Each `Role` holds the `db_config`,
`connection_specification_name`, `schema_cache` and `pool`. By default
the `RoleManager` holds a single reference from a `default` key to the
`Role` instance. A sharded/multi-tenant app can pass an optional second
argument to `remove_connection`, `retrieve_connection_pool`,
`establish_connection` and `connected?` on the handler, thus allowing
for multiple connections belonging to the same class/handler without
breaking backwards compatibility.

By using the `RoleManager` we can avoid altering the public API, moving
around handler/role concepts, and achieve the internal needs for
establishing multiple connections per handler per class.

**A note about why we opened this PR:**

We very much appreciate the work that went into #37388 and in no way mean
to diminish that work. However, it breaks the following public APIs:

* `#retrieve_connection`, `#connected?`, and `#remove_connection` are
public methods on handler and can't be changed from taking a spec to a
role.
* The knowledge that the handler keys are symbols relating to a role
(`:writing`/`:reading`) is public - changing how handlers are accessed
will break apps/libraries.

In addition it doesn't solve the problem of mapping a single connection
to a single class since it has a 1:1 mapping of `class (handler) -> role
(writing) -> db_config`. Multiple pools in a writing role can't exist
in that implementation.

The new PR solves this by using the `RoleManager` to hold multiple connection
objects for the same class. This lets a handler hold a role manager
which can hold as many roles for that writer as the app needs.

**Regarding the `Role` name:**

When I originally designed the API for multiple databases, it wasn't
accidental that handler and role are the same concept. Handler is the
internal concept (since that's what was there already) and Role was the
public external concept. Meaning, role and handler were meant to
be the same thing. The concept here means that when you switch a
handler/role, Rails automatically can pick up the connection on the
other role by knowing the specification name. Changing this would mean not
just that we need to rework how GitHub and many many gems work, but also
means retraining users of Rails 6.0 that all these concepts changed.

Since this PR doesn't move around the concepts in connection
management and instead creates an intermediary between `handler` and
`role` to manage the connection data (`db_config`, `schema_cache`,
`pool`, and `connection_specification`) we think that `Role` and
`RoleManager` are the wrong name.

We didn't change it yet in this PR because we wanted to keep change
churn low for initial review. We also haven't come up with a better
name yet. 😄

**What this PR does not solve:**

Our PR here solves a small portion of the problem - it allows models to
have multiple connections on a class. It doesn't aim to solve any other
problems than that. Going forward we'll need to still solve the
following problems:

* `DatabaseConfig` doesn't support a sharding configuration
* `connects_to`/`connected_to` still needs a way to switch connections for shards
* Automatic switching of shards
* `connection_specification_name` still exists

**The End**

Thanks for reading this far. These problems aren't easy to solve. John
and I spent a lot of time trying different things and so I hope that
this doesn't come across as if we think we know better. I would have
commented on the other PR what changes to make but we needed to try out
different solutions in order to get here.

Ultimately we're aiming to change as little as the API as possible. Even
if the handler/role -> manager -> db_config/pool/etc isn't how we'd
design connection management if we could start over, we also don't want
to break public APIs. It's important that we make things better while
maintaining compatibility.

The `RoleManager` class makes it possible for us to fix the underlying
problem while maintaining all the backwards compatibility in the public
API.

We all have the same goal; to add sharding support to Rails. Let me know
your thoughts on this change in lieu of #37388 and if you have questions.

Co-authored-by: John Crepezzi <seejohnrun@github.com>
2019-11-05 16:27:56 -05:00
..
bin
examples
lib Add an intermediary called RoleManager to manage connections 2019-11-05 16:27:56 -05:00
test Add an intermediary called RoleManager to manage connections 2019-11-05 16:27:56 -05:00
.gitignore
activerecord.gemspec Add bug tracker/documentation/mailing list URIs to the gemspecs 2019-10-11 20:47:19 -04:00
CHANGELOG.md Add changelog for IN empty array 2019-10-25 15:22:58 -07:00
MIT-LICENSE Bump license years for 2019 2018-12-31 10:24:38 +07:00
Rakefile Clean up AR's isolated test runner 2019-03-05 02:02:41 +10:30
README.rdoc Fix docs to update http and www.ruby-doc.org [ci skip] 2019-09-09 09:34:09 +09:00
RUNNING_UNIT_TESTS.rdoc Fix links in gemspec and docs from http to https. 2019-03-09 19:42:35 +05:30

= Active Record -- Object-relational mapping in Rails

Active Record connects classes to relational database tables to establish an
almost zero-configuration persistence layer for applications. The library
provides a base class that, when subclassed, sets up a mapping between the new
class and an existing table in the database. In the context of an application,
these classes are commonly referred to as *models*. Models can also be
connected to other models; this is done by defining *associations*.

Active Record relies heavily on naming in that it uses class and association
names to establish mappings between respective database tables and foreign key
columns. Although these mappings can be defined explicitly, it's recommended
to follow naming conventions, especially when getting started with the
library.

You can read more about Active Record in the {Active Record Basics}[https://edgeguides.rubyonrails.org/active_record_basics.html] guide.

A short rundown of some of the major features:

* Automated mapping between classes and tables, attributes and columns.

   class Product < ActiveRecord::Base
   end

  {Learn more}[link:classes/ActiveRecord/Base.html]

The Product class is automatically mapped to the table named "products",
which might look like this:

   CREATE TABLE products (
     id bigint NOT NULL auto_increment,
     name varchar(255),
     PRIMARY KEY  (id)
   );

This would also define the following accessors: <tt>Product#name</tt> and
<tt>Product#name=(new_name)</tt>.


* Associations between objects defined by simple class methods.

   class Firm < ActiveRecord::Base
     has_many   :clients
     has_one    :account
     belongs_to :conglomerate
   end

  {Learn more}[link:classes/ActiveRecord/Associations/ClassMethods.html]


* Aggregations of value objects.

   class Account < ActiveRecord::Base
     composed_of :balance, class_name: 'Money',
                 mapping: %w(balance amount)
     composed_of :address,
                 mapping: [%w(address_street street), %w(address_city city)]
   end

  {Learn more}[link:classes/ActiveRecord/Aggregations/ClassMethods.html]


* Validation rules that can differ for new or existing objects.

    class Account < ActiveRecord::Base
      validates :subdomain, :name, :email_address, :password, presence: true
      validates :subdomain, uniqueness: true
      validates :terms_of_service, acceptance: true, on: :create
      validates :password, :email_address, confirmation: true, on: :create
    end

  {Learn more}[link:classes/ActiveRecord/Validations.html]


* Callbacks available for the entire life cycle (instantiation, saving, destroying, validating, etc.).

   class Person < ActiveRecord::Base
     before_destroy :invalidate_payment_plan
     # the `invalidate_payment_plan` method gets called just before Person#destroy
   end

  {Learn more}[link:classes/ActiveRecord/Callbacks.html]


* Inheritance hierarchies.

   class Company < ActiveRecord::Base; end
   class Firm < Company; end
   class Client < Company; end
   class PriorityClient < Client; end

  {Learn more}[link:classes/ActiveRecord/Base.html]


* Transactions.

    # Database transaction
    Account.transaction do
      david.withdrawal(100)
      mary.deposit(100)
    end

  {Learn more}[link:classes/ActiveRecord/Transactions/ClassMethods.html]


* Reflections on columns, associations, and aggregations.

    reflection = Firm.reflect_on_association(:clients)
    reflection.klass # => Client (class)
    Firm.columns # Returns an array of column descriptors for the firms table

  {Learn more}[link:classes/ActiveRecord/Reflection/ClassMethods.html]


* Database abstraction through simple adapters.

    # connect to SQLite3
    ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: 'dbfile.sqlite3')

    # connect to MySQL with authentication
    ActiveRecord::Base.establish_connection(
      adapter:  'mysql2',
      host:     'localhost',
      username: 'me',
      password: 'secret',
      database: 'activerecord'
    )

  {Learn more}[link:classes/ActiveRecord/Base.html] and read about the built-in support for
  MySQL[link:classes/ActiveRecord/ConnectionAdapters/Mysql2Adapter.html],
  PostgreSQL[link:classes/ActiveRecord/ConnectionAdapters/PostgreSQLAdapter.html], and
  SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html].


* Logging support for Log4r[https://github.com/colbygk/log4r] and Logger[https://ruby-doc.org/stdlib/libdoc/logger/rdoc/].

    ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
    ActiveRecord::Base.logger = Log4r::Logger.new('Application Log')


* Database agnostic schema management with Migrations.

    class AddSystemSettings < ActiveRecord::Migration[5.0]
      def up
        create_table :system_settings do |t|
          t.string  :name
          t.string  :label
          t.text    :value
          t.string  :type
          t.integer :position
        end

        SystemSetting.create name: 'notice', label: 'Use notice?', value: 1
      end

      def down
        drop_table :system_settings
      end
    end

  {Learn more}[link:classes/ActiveRecord/Migration.html]


== Philosophy

Active Record is an implementation of the object-relational mapping (ORM)
pattern[https://www.martinfowler.com/eaaCatalog/activeRecord.html] by the same
name described by Martin Fowler:

  "An object that wraps a row in a database table or view,
  encapsulates the database access, and adds domain logic on that data."

Active Record attempts to provide a coherent wrapper as a solution for the inconvenience that is
object-relational mapping. The prime directive for this mapping has been to minimize
the amount of code needed to build a real-world domain model. This is made possible
by relying on a number of conventions that make it easy for Active Record to infer
complex relations and structures from a minimal amount of explicit direction.

Convention over Configuration:
* No XML files!
* Lots of reflection and run-time extension
* Magic is not inherently a bad word

Admit the Database:
* Lets you drop down to SQL for odd cases and performance
* Doesn't attempt to duplicate or replace data definitions


== Download and installation

The latest version of Active Record can be installed with RubyGems:

  $ gem install activerecord

Source code can be downloaded as part of the Rails project on GitHub:

* https://github.com/rails/rails/tree/master/activerecord


== License

Active Record is released under the MIT license:

* https://opensource.org/licenses/MIT


== Support

API documentation is at:

* https://api.rubyonrails.org

Bug reports for the Ruby on Rails project can be filed here:

* https://github.com/rails/rails/issues

Feature requests should be discussed on the rails-core mailing list here:

* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core