Merge pull request #40169 from eileencodes/ensure-that-default-is-considered-primary-or-first

Default db_config should be primary or first
This commit is contained in:
Eileen M. Uchitelle 2020-09-03 17:08:25 -04:00 committed by GitHub
commit 77667bcead
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 33 deletions

View File

@ -1,3 +1,9 @@
* Ensure the default configuration is considered primary or first for an environment
If a multiple database application provides a configuration named primary, that will be treated as default. In applications that do not have a primary entry, the default database configuration will be the first configuration for an environment.
*Eileen M. Uchitelle*
* Allow `where` references association names as joined table name aliases.
```ruby

View File

@ -87,6 +87,20 @@ module ActiveRecord
end
end
# A primary configuration is one that is named primary or if there is
# no primary, the first configuration for an environment will be treated
# as primary. This is used as the "default" configuration and is used
# when the application needs to treat one configuration differently. For
# example, when Rails dumps the schema, the primary configuration's schema
# file will be named `schema.rb` instead of `primary_schema.rb`.
def primary?(name) # :nodoc:
return true if name == "primary"
first_config = find_db_config(default_env)
first_config && name == first_config.name
end
# Returns the DatabaseConfigurations object as a Hash.
def to_h
configurations.inject({}) do |memo, db_config|

View File

@ -407,21 +407,21 @@ module ActiveRecord
end
end
def dump_filename(name, format = ActiveRecord::Base.schema_format)
filename = if name == "primary"
def dump_filename(db_config_name, format = ActiveRecord::Base.schema_format)
filename = if ActiveRecord::Base.configurations.primary?(db_config_name)
schema_file_type(format)
else
"#{name}_#{schema_file_type(format)}"
"#{db_config_name}_#{schema_file_type(format)}"
end
ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename)
end
def cache_dump_filename(name, schema_cache_path: nil)
filename = if name == "primary"
def cache_dump_filename(db_config_name, schema_cache_path: nil)
filename = if ActiveRecord::Base.configurations.primary?(db_config_name)
"schema_cache.yml"
else
"#{name}_schema_cache.yml"
"#{db_config_name}_schema_cache.yml"
end
schema_cache_path || ENV["SCHEMA_CACHE"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename)

View File

@ -21,9 +21,9 @@ so you don't have to store your data all in one place.
At this time the following features are supported:
* Multiple primary databases and a replica for each
* Multiple writer databases and a replica for each
* Automatic connection switching for the model you're working with
* Automatic swapping between the primary and replica depending on the HTTP verb
* Automatic swapping between the writer and replica depending on the HTTP verb
and recent writes
* Rails tasks for creating, dropping, migrating, and interacting with the multiple
databases
@ -40,7 +40,7 @@ The following features are not (yet) supported:
While Rails tries to do most of the work for you there are still some steps you'll
need to do to get your application ready for multiple databases.
Let's say we have an application with a single primary database and we need to add a
Let's say we have an application with a single writer database and we need to add a
new database for some new tables we're adding. The name of the new database will be
"animals".
@ -53,8 +53,15 @@ production:
adapter: mysql
```
Let's add a replica for the primary, a new writer called animals and a replica for that
as well. To do this we need to change our `database.yml` from a 2-tier to a 3-tier config.
Let's add a replica for the first configuration, and a second database called animals and a
replica for that as well. To do this we need to change our `database.yml` from a 2-tier
to a 3-tier config.
If a primary configuration is provided this will be used as the "default" configuration. If
there is no configuration named "primary" Rails will use the first configuration for an
environment. The default configurations will use the default Rails filenames. For example
primary configurations will use `schema.rb` for the schema file whereas all other entries
will use `[CONFIGURATION_NAMESPACE]_schema.rb` for the filename.
```yaml
production:
@ -81,15 +88,17 @@ production:
When using multiple databases there are a few important settings.
First, the database name for the primary and replica should be the same because they contain
the same data. Second, the username for the primary and replica should be different, and the
replica user's permissions should be to read and not write.
First, the database name for the `primary` and `primary_replica` should be the same because they contain
the same data. This is also the case for `animals` and `animals_replica`.
Second, the username for the writers and replicas should be different, and the
replica user's permissions should be set to only read and not write.
When using a replica database you need to add a `replica: true` entry to the replica in the
`database.yml`. This is because Rails otherwise has no way of knowing which one is a replica
and which one is the primary.
and which one is the writer.
Lastly, for new primary databases you need to set the `migrations_paths` to the directory
Lastly, for new writer databases you need to set the `migrations_paths` to the directory
where you will store migrations for that database. We'll look more at `migrations_paths`
later on in this guide.
@ -227,13 +236,13 @@ use a different parent class.
Finally, in order to use the read-only replica in your application you'll need to activate
the middleware for automatic switching.
Automatic switching allows the application to switch from the primary to replica or replica
to primary based on the HTTP verb and whether there was a recent write.
Automatic switching allows the application to switch from the writer to replica or replica
to writer based on the HTTP verb and whether there was a recent write.
If the application is receiving a POST, PUT, DELETE, or PATCH request the application will
automatically write to the primary. For the specified time after the write, the application
will read from the primary. For a GET or HEAD request the application will read from the
replica unless there was a recent write.
automatically write to the writer database. For the specified time after the write, the
application will read from the primary. For a GET or HEAD request the application will read
from the replica unless there was a recent write.
To activate the automatic connection switching middleware, add or uncomment the following
lines in your application config.
@ -245,7 +254,7 @@ config.active_record.database_resolver_context = ActiveRecord::Middleware::Datab
```
Rails guarantees "read your own write" and will send your GET or HEAD request to the
primary if it's within the `delay` window. By default the delay is set to 2 seconds. You
writer if it's within the `delay` window. By default the delay is set to 2 seconds. You
should change this based on your database infrastructure. Rails doesn't guarantee "read
a recent write" for other users within the delay window and will send GET and HEAD requests
to the replicas unless they wrote recently.
@ -274,7 +283,7 @@ config.active_record.database_resolver_context = MyCookieResolver
## Using manual connection switching
There are some cases where you may want your application to connect to a primary or a replica
There are some cases where you may want your application to connect to a writer or a replica
and the automatic connection switching isn't adequate. For example, you may know that for a
particular request you always want to send the request to a replica, even when you are in a
POST request path.

View File

@ -343,15 +343,12 @@ module ApplicationTests
db_migrate_and_status database_url_db_name
end
def db_schema_dump(database: nil)
def db_schema_dump
Dir.chdir(app_path) do
args = ["generate", "model", "book", "title:string"]
args << "--database=#{database}" if database
rails args
rails "db:migrate", "db:schema:dump"
dump_name = database ? "#{database}_schema.rb" : "schema.rb"
schema_dump = File.read("db/#{dump_name}")
assert_match(/create_table \"books\"/, schema_dump)
assert_match(/create_table \"books\"/, File.read("db/schema.rb"))
end
end
@ -427,17 +424,17 @@ module ApplicationTests
statement_timeout: 1000
development:
some_entry:
<<: *default
database: db/development_other.sqlite3
migrations_paths: db/some_entry_migrate
primary:
<<: *default
database: db/development.sqlite3
another_entry:
<<: *default
database: db/another_entry_development.sqlite3
migrations_paths: db/another_entry_migrate
YAML
end
end
db_schema_dump(database: "some_entry")
db_schema_dump
db_schema_cache_dump
end

View File

@ -812,6 +812,21 @@ module ApplicationTests
db_create_and_drop_namespace("primary", "db/development.sqlite3")
end
test "a thing" do
app_file "config/database.yml", <<-YAML
development:
default:
database: db/default.sqlite3
adapter: sqlite3
animals:
database: db/develoment_animals.sqlite3
adapter: sqlite3
migrations_paths: db/animals_migrate
YAML
db_migrate_and_schema_dump_and_load
end
end
end
end