From ee9e308f686ac1c7b0714671a1dcf1d96de87d70 Mon Sep 17 00:00:00 2001 From: eileencodes Date: Wed, 2 Sep 2020 14:15:20 -0400 Subject: [PATCH] Default db_config should be primary or first The handling for single database applications has always set a schema.rb or structure.sql files for loading the database schema. When we first implemented multiple database support we intended to keep this for the original, default database. Afterall Rails _has_ to connect to something on boot. In development only one connection is connected on boot since we don't eager load the app. Originally we had thought that all applications should be required to add a `primary` entry in the database configurations file. However, this hasn't worked in practice and we have some code now that does not assume there's a primary. The schema dumping/loading code however, still assumed there was a "primary" in the configurations file. We want the "default" database in any application to use the original files even when converted to a multiple database application as this reduces the need to make changes when implementing this functionality on an existing application. The changes here update Rails to ensure that we treat either "primary" or the first database configuration for an environment as "default". If there is a "primary" that will be used as the default configuration. If there is no primary the configuration that is first for an environment will be used as the default. For schema dump/load this means that the default configuration (primary or first) will use `schema.rb` as the filename and other configurations will use `[CONFIGURATION_NAME]_schema.rb`. This should also help us finish the pull request to infer migrations paths since now we can say the first configuration is the default. This is a natural assumption for application developers. Followup to #39536 --- activerecord/CHANGELOG.md | 6 +++ .../active_record/database_configurations.rb | 14 ++++++ .../lib/active_record/tasks/database_tasks.rb | 12 +++--- .../active_record_multiple_databases.md | 43 +++++++++++-------- railties/test/application/rake/dbs_test.rb | 17 +++----- .../test/application/rake/multi_dbs_test.rb | 15 +++++++ 6 files changed, 74 insertions(+), 33 deletions(-) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 2f671cdfca..77b94e098d 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -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 diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index 0c4f478876..5fb60758d4 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -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| diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 46656373fd..14e55aa2b7 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -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) diff --git a/guides/source/active_record_multiple_databases.md b/guides/source/active_record_multiple_databases.md index 7813820274..03d0c49518 100644 --- a/guides/source/active_record_multiple_databases.md +++ b/guides/source/active_record_multiple_databases.md @@ -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. diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 8fcf119637..6bd001e5c9 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -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 diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb index f6a542e5ff..745c3bdacd 100644 --- a/railties/test/application/rake/multi_dbs_test.rb +++ b/railties/test/application/rake/multi_dbs_test.rb @@ -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