edge
詳細はrubyonrails.orgで: もっとRuby on Rails

複数のデータベースをActive Recordで使用する

このガイドでは、Railsアプリケーションで複数のデータベースを使用する方法について説明します。

このガイドを読み終えると、以下のことがわかります。

アプリケーションが人気を集め、利用が増えるにつれて、新しいユーザーとそのデータをサポートするためにアプリケーションをスケールする必要があります。アプリケーションがスケールする方法の1つは、データベースレベルでのスケールです。Railsは現在、複数のデータベースをサポートしているため、データを1つの場所にすべて保存する必要はありません。

現時点では、以下の機能がサポートされています。

  • 複数のライターデータベースとそれぞれのレプリカ
  • モデルごとの自動接続切り替え
  • HTTP動詞と最近の書き込みに応じたライターとレプリカの自動切り替え
  • 複数のデータベースの作成、削除、マイグレーション、および操作のためのRailsタスク

以下の機能は(まだ)サポートされていません。

  • レプリカの負荷分散

1 アプリケーションの設定

Railsはほとんどの作業を自動化しようとしますが、複数のデータベースに対応するためにはまだいくつかの手順が必要です。

新しいテーブルを追加するために新しいデータベースを追加する必要があるとします。新しいデータベースの名前は「animals」です。

database.ymlは次のようになります。

production:
  database: my_primary_database
  adapter: mysql2
  username: root
  password: <%= ENV['ROOT_PASSWORD'] %>

最初の設定にレプリカを追加し、animalsという名前の2番目のデータベースとそのレプリカも追加します。これを行うには、database.ymlを2層から3層の設定に変更する必要があります。

プライマリの設定が提供された場合、それは「デフォルト」の設定として使用されます。"primary"という名前の設定がない場合、Railsは各環境のデフォルトの設定として最初の設定を使用します。デフォルトの設定は、デフォルトのRailsのファイル名を使用します。たとえば、プライマリの設定はスキーマファイルにschema.rbを使用し、他のすべてのエントリはファイル名に[CONFIGURATION_NAMESPACE]_schema.rbを使用します。

production:
  primary:
    database: my_primary_database
    username: root
    password: <%= ENV['ROOT_PASSWORD'] %>
    adapter: mysql2
  primary_replica:
    database: my_primary_database
    username: root_readonly
    password: <%= ENV['ROOT_READONLY_PASSWORD'] %>
    adapter: mysql2
    replica: true
  animals:
    database: my_animals_database
    username: animals_root
    password: <%= ENV['ANIMALS_ROOT_PASSWORD'] %>
    adapter: mysql2
    migrations_paths: db/animals_migrate
  animals_replica:
    database: my_animals_database
    username: animals_readonly
    password: <%= ENV['ANIMALS_READONLY_PASSWORD'] %>
    adapter: mysql2
    replica: true

複数のデータベースを使用する場合、いくつかの重要な設定があります。

まず、primaryprimary_replicaのデータベース名は同じである必要があります。これはanimalsanimals_replicaの場合も同様です。

次に、ライターとレプリカのユーザー名は異なる必要があり、レプリカユーザーのデータベースの権限は読み取りのみに設定する必要があります。

レプリカデータベースを使用する場合、database.ymlのレプリカにreplica: trueのエントリを追加する必要があります。これは、Railsがどちらがレプリカでどちらがライターかを知る方法がないためです。Railsはレプリカに対してマイグレーションなどの特定のタスクを実行しません。

最後に、新しいライターデータベースでは、migrations_pathsをそのデータベースのマイグレーションを保存するディレクトリに設定する必要があります。このガイドの後半でmigrations_pathsについて詳しく見ていきます。

新しいデータベースができたので、接続モデルを設定しましょう。新しいデータベースを使用するために、新しい抽象クラスを作成し、animalsデータベースに接続する必要があります。

class AnimalsRecord < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :animals, reading: :animals_replica }
end

次に、ApplicationRecordを更新して新しいレプリカを認識するようにします。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to database: { writing: :primary, reading: :primary_replica }
end

アプリケーションレコードに別の名前のクラスを使用する場合は、primary_abstract_classを設定する必要があります。これにより、RailsがActiveRecord::Baseがどのクラスと接続を共有するかを知ることができます。

class PrimaryApplicationRecord < ActiveRecord::Base
  primary_abstract_class
end

プライマリ/primary_replicaに接続するクラスは、標準のRailsアプリケーションと同様に、プライマリの抽象クラスを継承することができます。 ruby class Person < ApplicationRecord end

デフォルトでは、Railsはプライマリとレプリカのデータベースロールをwritingreadingと予想しています。もし既存のシステムがある場合、変更したくない設定がすでにあるかもしれません。その場合、アプリケーションの設定で新しいロール名を設定することができます。

config.active_record.writing_role = :default
config.active_record.reading_role = :readonly

テーブルに対して複数の個別のモデルを同じデータベースに接続する代わりに、単一のモデルに接続してからそのモデルを継承することが重要です。データベースクライアントにはオープンできる接続数の制限があり、これを行うと接続数が増えてしまいます。なぜなら、Railsは接続仕様名にモデルクラス名を使用するからです。

database.ymlと新しいモデルが設定されたので、データベースを作成する時が来ました。Rails 6.0には、Railsで複数のデータベースを使用するために必要なすべてのrailsタスクが付属しています。

bin/rails -Tを実行すると、実行可能なすべてのコマンドが表示されます。次のような結果が表示されるはずです。

$ bin/rails -T
bin/rails db:create                          # DATABASE_URLまたはconfig/database.ymlに基づいてデータベースを作成します...
bin/rails db:create:animals                  # 現在の環境のanimalsデータベースを作成します
bin/rails db:create:primary                  # 現在の環境のプライマリデータベースを作成します
bin/rails db:drop                            # DATABASE_URLまたはconfig/database.ymlに基づいてデータベースを削除します...
bin/rails db:drop:animals                    # 現在の環境のanimalsデータベースを削除します
bin/rails db:drop:primary                    # 現在の環境のプライマリデータベースを削除します
bin/rails db:migrate                         # データベースをマイグレーションします(オプション:VERSION=x、VERBOSE=false、SCOPE=blog)
bin/rails db:migrate:animals                 # 現在の環境のanimalsデータベースをマイグレーションします
bin/rails db:migrate:primary                 # 現在の環境のプライマリデータベースをマイグレーションします
bin/rails db:migrate:status                  # マイグレーションのステータスを表示します
bin/rails db:migrate:status:animals          # animalsデータベースのマイグレーションのステータスを表示します
bin/rails db:migrate:status:primary          # プライマリデータベースのマイグレーションのステータスを表示します
bin/rails db:reset                           # 現在の環境のスキーマからすべてのデータベースを削除して再作成し、シードをロードします
bin/rails db:reset:animals                   # 現在の環境のanimalsデータベースをスキーマから削除して再作成し、シードをロードします
bin/rails db:reset:primary                   # 現在の環境のプライマリデータベースをスキーマから削除して再作成し、シードをロードします
bin/rails db:rollback                        # スキーマを前のバージョンにロールバックします(ステップを指定する場合はSTEP=n)
bin/rails db:rollback:animals                # 現在の環境のanimalsデータベースをロールバックします(ステップを指定する場合はSTEP=n)
bin/rails db:rollback:primary                # 現在の環境のプライマリデータベースをロールバックします(ステップを指定する場合はSTEP=n)
bin/rails db:schema:dump                     # データベーススキーマファイルを作成します(db/schema.rbまたはdb/structure.sqlのいずれか...
bin/rails db:schema:dump:animals             # データベーススキーマファイルを作成します(db/schema.rbまたはdb/structure.sqlのいずれか...
bin/rails db:schema:dump:primary             # 任意のDBでポータブルなdb/schema.rbファイルを作成します...
bin/rails db:schema:load                     # データベーススキーマファイルをロードします(db/schema.rbまたはdb/structure.sqlのいずれか...
bin/rails db:schema:load:animals             # データベーススキーマファイルをロードします(db/schema.rbまたはdb/structure.sqlのいずれか...
bin/rails db:schema:load:primary             # データベーススキーマファイルをロードします(db/schema.rbまたはdb/structure.sqlのいずれか...
bin/rails db:setup                           # すべてのデータベースを作成し、すべてのスキーマをロードし、シードデータで初期化します(最初にすべてのデータベースを削除するにはdb:resetを使用します)
bin/rails db:setup:animals                   # animalsデータベースを作成し、スキーマをロードし、シードデータで初期化します(最初にデータベースを削除するにはdb:reset:animalsを使用します)
bin/rails db:setup:primary                   # プライマリデータベースを作成し、スキーマをロードし、シードデータで初期化します(最初にデータベースを削除するにはdb:reset:primaryを使用します)

bin/rails db:createのようなコマンドを実行すると、プライマリとanimalsの両方のデータベースが作成されます。データベースユーザーを作成するコマンドは存在せず、レプリカのための読み取り専用ユーザーをサポートするために手動で作成する必要があります。animalsデータベースだけを作成したい場合は、bin/rails db:create:animalsを実行できます。

2 スキーマとマイグレーションを管理せずにデータベースに接続する

スキーマ管理、マイグレーション、シードなどのデータベース管理タスクなしで外部データベースに接続したい場合は、データベースごとの設定オプションdatabase_tasks: falseを設定できます。デフォルトではtrueに設定されています。

production:
  primary:
    database: my_database
    adapter: mysql2
  animals:
    database: my_animals_database
    adapter: mysql2
    database_tasks: false

3 ジェネレータとマイグレーション

複数のデータベースのマイグレーションは、設定のデータベースキーの前にプレフィックスが付いた独自のフォルダに配置する必要があります。 Railsでは、migrations_pathsをデータベースの設定に設定する必要もあります。これにより、Railsにマイグレーションを見つける場所を指示することができます。

たとえば、animalsデータベースの場合、マイグレーションはdb/animals_migrateディレクトリを探します。また、primaryデータベースの場合はdb/migrateを探します。Railsのジェネレータは、--databaseオプションを受け取るようになりましたので、正しいディレクトリにファイルが生成されます。以下のようにコマンドを実行できます。

$ bin/rails generate migration CreateDogs name:string --database animals

Railsのジェネレータを使用している場合、スキャフォールドやモデルのジェネレータは抽象クラスを自動的に作成します。コマンドラインにデータベースのキーを渡すだけです。

$ bin/rails generate scaffold Dog name:string --database animals

データベース名とRecordのクラスが作成されます。この例ではデータベース名はAnimalsなので、AnimalsRecordとなります。

class AnimalsRecord < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :animals }
end

生成されたモデルは自動的にAnimalsRecordを継承します。

class Dog < AnimalsRecord
end

注意:Railsはライターのレプリカがどのデータベースかわからないため、これを抽象クラスに追加する必要があります。

Railsは新しいクラスを一度だけ生成します。新しいスキャフォールドで上書きされたり、スキャフォールドが削除されたりしても、クラスは削除されません。

既に抽象クラスがあり、その名前がAnimalsRecordと異なる場合は、--parentオプションを渡して別の抽象クラスを使用することができます。

$ bin/rails generate scaffold Dog name:string --database animals --parent Animals::Record

これにより、AnimalsRecordの生成がスキップされます。異なる親クラスを使用することをRailsに指示したためです。

4 自動ロール切り替えの有効化

最後に、アプリケーションで読み取り専用レプリカを使用するためには、自動切り替えのミドルウェアを有効にする必要があります。

自動切り替えにより、アプリケーションはHTTPの動詞と、リクエストユーザーによる最近の書き込みの有無に基づいて、ライターからレプリカまたはレプリカからライターに切り替えることができます。

アプリケーションがPOST、PUT、DELETE、またはPATCHリクエストを受け取る場合、アプリケーションは自動的にライターデータベースに書き込みます。書き込み後の指定された時間内は、アプリケーションはプライマリから読み取ります。GETまたはHEADリクエストの場合、アプリケーションは最近の書き込みがない限り、レプリカから読み取ります。

自動接続切り替えミドルウェアを有効にするには、自動切り替えジェネレータを実行します。

$ bin/rails g active_record:multi_db

次に、次の行のコメントを解除します。

Rails.application.configure do
  config.active_record.database_selector = { delay: 2.seconds }
  config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
  config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end

Railsは「自分の書き込みを読む」と保証し、delayウィンドウ内であればGETまたはHEADリクエストをライターに送信します。デフォルトでは、遅延は2秒に設定されています。データベースインフラストラクチャに基づいてこれを変更する必要があります。Railsは遅延ウィンドウ内の他のユーザーに対して「最近の書き込みを読む」とは保証せず、GETおよびHEADリクエストを最近書き込んだ場合を除いてレプリカに送信します。

Railsの自動接続切り替えは比較的原始的で、意図的にあまり多くのことを行いません。目標は、アプリケーション開発者がカスタマイズできる柔軟な自動接続切り替えの方法を示すシステムです。

Railsの設定では、切り替えの方法や基準を簡単に変更できます。たとえば、セッションではなくクッキーを使用して接続を切り替える場合は、独自のクラスを作成できます。

class MyCookieResolver << ActiveRecord::Middleware::DatabaseSelector::Resolver
  def self.call(request)
    new(request.cookies)
  end

  def initialize(cookies)
    @cookies = cookies
  end

  attr_reader :cookies

  def last_write_timestamp
    self.class.convert_timestamp_to_time(cookies[:last_write])
  end

  def update_last_write_timestamp
    cookies[:last_write] = self.class.convert_time_to_timestamp(Time.now)
  end

  def save(response)
  end
end

そして、ミドルウェアに渡します。

config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = MyCookieResolver

5 手動接続切り替えの使用

自動接続切り替えが十分ではない場合、アプリケーションがライターまたはレプリカに接続する必要がある場合があります。たとえば、特定のリクエストでは、POSTリクエストパスであっても常にレプリカにリクエストを送信する必要がある場合などです。

この場合、Railsは必要な接続に切り替えるためのconnected_toメソッドを提供しています。 ruby ActiveRecord::Base.connected_to(role: :reading) do # このブロック内のすべてのコードは、readingのロールに接続されます end

connected_to呼び出しの「role」は、その接続ハンドラ(またはロール)に接続されている接続を検索します。reading接続ハンドラは、connects_toreadingというロール名で接続されたすべての接続を保持します。

connected_toにロールを指定すると、接続仕様名を使用して既存の接続を検索し、切り替えます。つまり、connected_to(role: :nonexistent)のような存在しないロールを渡すと、ActiveRecord::ConnectionNotEstablished (No connection pool for 'ActiveRecord::Base' found for the 'nonexistent' role.)というエラーが発生します。

クエリが読み取り専用であることをRailsに保証させるには、prevent_writes: trueを渡します。これにより、書き込みのように見えるクエリがデータベースに送信されないようになります。また、レプリカデータベースを読み取り専用モードで設定する必要もあります。

ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true) do
  # Railsは各クエリを読み取りクエリであることを確認します
end

6 水平シャーディング

水平シャーディングとは、データベースを分割して各データベースサーバーの行数を減らすが、「シャード」全体で同じスキーマを維持することです。これは一般的に「マルチテナント」シャーディングと呼ばれます。

Railsで水平シャーディングをサポートするためのAPIは、Rails 6.0以降存在している複数のデータベース/垂直シャーディングAPIと似ています。

シャードは、次のように3層の設定で宣言されます。

production:
  primary:
    database: my_primary_database
    adapter: mysql2
  primary_replica:
    database: my_primary_database
    adapter: mysql2
    replica: true
  primary_shard_one:
    database: my_primary_shard_one
    adapter: mysql2
  primary_shard_one_replica:
    database: my_primary_shard_one
    adapter: mysql2
    replica: true

次に、shardsキーを使用してモデルをconnects_to APIで接続します。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to shards: {
    default: { writing: :primary, reading: :primary_replica },
    shard_one: { writing: :primary_shard_one, reading: :primary_shard_one_replica }
  }
end

最初のシャード名としてdefaultを使用する必要はありません。Railsはconnects_toハッシュの最初のシャード名を「デフォルト」の接続として扱います。この接続は、スキーマがシャード全体で同じであるタイプデータやその他の情報を読み込むために内部的に使用されます。

その後、モデルはconnected_to APIを使用して接続を手動で切り替えることができます。シャーディングを使用する場合、roleshardの両方を渡す必要があります。

ActiveRecord::Base.connected_to(role: :writing, shard: :default) do
  @id = Person.create! # ":default"という名前のシャードにレコードを作成します
end

ActiveRecord::Base.connected_to(role: :writing, shard: :shard_one) do
  Person.find(@id) # レコードが見つからないため、存在しません。なぜなら、それは":default"という名前のシャードで作成されたからです。
end

水平シャーディングAPIは読み取りレプリカもサポートしています。connected_to APIでロールとシャードを切り替えることができます。

ActiveRecord::Base.connected_to(role: :reading, shard: :shard_one) do
  Person.first # シャード1の読み取りレプリカからレコードを検索します
end

7 自動シャード切り替えの有効化

アプリケーションは、提供されるミドルウェアを使用してリクエストごとに自動的にシャードを切り替えることができます。

ShardSelectorミドルウェアは、自動的にシャードを切り替えるためのフレームワークを提供します。Railsは、どのシャードに切り替えるかを決定するための基本的なフレームワークを提供し、必要に応じてアプリケーションがカスタムストラテジーを記述できるようにします。

ShardSelectorは、ミドルウェアが動作を変更するために使用できるオプションのセット(現在はlockのみサポート)を受け取ります。lockはデフォルトでtrueになっており、ブロック内に入るとリクエストがシャードの切り替えを禁止します。lockがfalseの場合、シャードの切り替えが許可されます。テナントベースのシャーディングでは、アプリケーションコードがテナント間を誤って切り替えることを防ぐために、lockは常にtrueにする必要があります。

自動シャード切り替えのためのファイルを生成するために、データベースセレクタと同じジェネレータを使用できます。

$ bin/rails g active_record:multi_db

次に、ファイルで次のコメントを解除します。

Rails.application.configure do
  config.active_record.shard_selector = { lock: true }
  config.active_record.shard_resolver = ->(request) { Tenant.find_by!(host: request.host).shard }
end

アプリケーションは、アプリケーション固有のモデルに依存するため、リゾルバのコードを提供する必要があります。例えば、リゾルバは次のようになります。

config.active_record.shard_resolver = ->(request) {
  subdomain = request.subdomain
  tenant = Tenant.find_by_subdomain!(subdomain)
  tenant.shard
}

8 細かいデータベース接続の切り替え

Rails 6.1では、すべてのデータベースをグローバルに切り替えるのではなく、1つのデータベースの接続を切り替えることができます。

細かいデータベース接続の切り替えでは、抽象的な接続クラスは他の接続に影響を与えることなく接続を切り替えることができます。これは、AnimalsRecordのクエリをレプリカから読み取るように切り替え、同時にApplicationRecordのクエリをプライマリに送信するために役立ちます。 ruby AnimalsRecord.connected_to(role: :reading) do Dog.first # animals_replicaから読み込み Person.first # primaryから読み込み end

シャードごとに接続を切り替えることも可能です。

AnimalsRecord.connected_to(role: :reading, shard: :shard_one) do
  Dog.first # shard_one_replicaから読み込み。shard_one_replicaに接続が存在しない場合、ConnectionNotEstablishedエラーが発生します
  Person.first # primary writerから読み込み
end

プライマリデータベースクラスタのみを切り替える場合は、ApplicationRecordを使用します。

ApplicationRecord.connected_to(role: :reading, shard: :shard_one) do
  Person.first # primary_shard_one_replicaから読み込み
  Dog.first # animals_primaryから読み込み
end

ActiveRecord::Base.connected_toを使用すると、接続をグローバルに切り替えることができます。

8.1 データベース間の結合を伴う関連の処理

Rails 7.0以降、Active Recordには、複数のデータベースを結合する関連を処理するオプションがあります。disable_joins: trueオプションを渡すと、2つ以上のクエリを実行して結合を無効化することができます。

例えば:

class Dog < AnimalsRecord
  has_many :treats, through: :humans, disable_joins: true
  has_many :humans

  has_one :home
  has_one :yard, through: :home, disable_joins: true
end

class Home
  belongs_to :dog
  has_one :yard
end

class Yard
  belongs_to :home
end

以前は、disable_joinsなしで@dog.treatsを呼び出すか、disable_joinsなしで@dog.yardを呼び出すとエラーが発生しました。これは、データベースがクラスタ間の結合を処理できないためです。disable_joinsオプションを使用すると、Railsはクラスタ間の結合を試みずに、複数のSELECTクエリを生成します。上記の関連では、@dog.treatsは次のSQLを生成します:

SELECT "humans"."id" FROM "humans" WHERE "humans"."dog_id" = ?  [["dog_id", 1]]
SELECT "treats".* FROM "treats" WHERE "treats"."human_id" IN (?, ?, ?)  [["human_id", 1], ["human_id", 2], ["human_id", 3]]

一方、@dog.yardは次のSQLを生成します:

SELECT "home"."id" FROM "homes" WHERE "homes"."dog_id" = ? [["dog_id", 1]]
SELECT "yards".* FROM "yards" WHERE "yards"."home_id" = ? [["home_id", 1]]

このオプションにはいくつかの重要な注意点があります:

  1. クエリが2つ以上実行されるため、パフォーマンスに影響がある場合があります(関連によります)。humansのSELECTが多数のIDを返した場合、treatsのSELECTは多すぎるIDを送信する可能性があります。
  2. 結合を行わなくなったため、順序や制限のあるクエリはインメモリでソートされます。1つのテーブルの順序は他のテーブルに適用できないためです。
  3. この設定は、結合を無効化したいすべての関連に追加する必要があります。Railsは関連の読み込みが遅延されるため、@dog.treatstreatsを読み込むためには、すでに生成するべきSQLを知っている必要があります。

8.2 スキーマキャッシュ

各データベースにスキーマキャッシュをロードする場合は、各データベースの設定でschema_cache_pathを設定し、アプリケーションの設定でconfig.active_record.lazily_load_schema_cache = trueを設定する必要があります。データベース接続が確立されると、キャッシュは遅延してロードされます。

9 注意点

9.1 レプリカの負荷分散

Railsはレプリカの自動的な負荷分散もサポートしていません。これは非常にインフラストラクチャに依存します。将来的には基本的なプリミティブな負荷分散を実装するかもしれませんが、スケールの大きなアプリケーションでは、Railsの外部で処理する必要があります。

フィードバック

このガイドの品質向上にご協力ください。

タイポや事実の誤りを見つけた場合は、ぜひ貢献してください。 開始するには、ドキュメントへの貢献セクションを読んでください。

不完全なコンテンツや最新でない情報も見つかるかもしれません。 メインのドキュメントに不足しているドキュメントを追加してください。 修正済みかどうかは、まずEdge Guidesを確認してください。 スタイルと規約については、Ruby on Rails Guides Guidelinesを確認してください。

修正すべき点を見つけたが、自分で修正できない場合は、 問題を報告してください

そして最後に、Ruby on Railsのドキュメントに関するあらゆる議論は、公式のRuby on Railsフォーラムで大歓迎です。