SQLite on Railsシリーズ(01)Gitブランチごとにデータベースを切り替える(翻訳)
本記事は、Ruby on Railsアプリケーションの強化方法を紹介するシリーズ記事の第1弾です。具体的には、私の最初のシリーズ記事として、SQLiteをRailsアプリケーションのデータベースエンジンとして活用するかたちで強化する方法を詳しく説明します。
最初の記事は、SQLiteをデータベースエンジンとして採用することで、ローカル開発ワークフローにおいて強力かつ新しい可能性が開かれること、具体的には、Gitブランチを切り替えるとデータベースも切り替わるGitブランチ固有のデータベースを作成・利用可能になることを詳しく見ていきます。
以下の記事に示したように、ここ数年production環境のWebアプリケーションでSQLiteを利用することへの関心が高まっています。
- 参考: Switching from etcd to SQLite: A Database Migration Story
- 参考: Use SQLite in production :: DjangoCon Europe 2023 :: pretalx
- 参考: Ask HN: Who uses SQLite database in production? | Hacker News
私もこれに全面的に賛成いたします。SQLiteはネットワーク遅延をなくして操作をシンプルにし、production環境の技術スタックに対する自動テストをやりやすくします。
本シリーズでは、SQLiteの操作をずっと強力かつ快適にするためにRailsアプリケーションで行える重要な微調整をいくつか説明したいと思います。まず、ローカル開発における開発者エクスペリエンス( DX )の向上に目を向けたいと思います。
Railsアプリケーション開発にチームで取り組んだことがある人なら誰でも知っているように、開発中のデータベーススキーマの管理は厄介なことになる可能性があります。
個別の開発者の手元にあるGitブランチには、スキーマを更新するマイグレーションも含まれている場合がありますが、ローカル環境にある開発用データベースは1つしかありません。そのため、開発中にブランチを切り替えるたびにデータベースも切り替えなければならなくなり、マイグレーションの実行も繁雑です。リリースに含めてはならないスキーマ変更がうっかりマージされると、production環境でバグが混入する可能性があります。
私がPlanetScaleで気に入っている機能の1つがブランチ機能です。
ブランチとは、互いに完全に異なった環境に存在するデータベーススキーマのことです。プロジェクトでコードのブランチを管理するのと同じように、あるブランチに変更を加えても、他のブランチをマージするまでは他のブランチに影響しません。
これは素晴らしい機能ですし、上記のつらみを解決してくれます。しかし私に言わせれば完璧とは言えません。この方法では、コード(マイグレーションを含む)を分離するGitブランチと、スキーマを分離するデータベースブランチという2種類の「ブランチ」がアプリに存在するという二重管理状態になります。
2種類の異なるブランチがあると、同期の問題が発生します。Gitブランチとデータベースブランチをどのように連携させるか、Gitブランチのマージをデータベースブランチのマージに紐づけてproduction環境への展開を行うにはどうすればよいか、などです。
これはPlanetScaleにおける必然的なトレードオフです。同社のサーバーレスデータベースプラットフォームは、すべてのユーザーのコードベースと深く統合するわけにはいかないからです。
しかしSQLiteを使うRails開発者なら、SQLiteならではの方法を使えるのです。
まずは理想的なシナリオを説明し、次にそれをどのように実装するかを詳しく見ていきましょう。必要なのは、コードとスキーマの両方を分離する単一のブランチ(Gitブランチ)を持つことです。
欲しいのは以下の機能です。
- Gitブランチを切り替えれば、スキーマブランチも自動的に切り替わる
- Gitブランチをマージするとproduction環境にデプロイされ、予測可能で安定したproductionスキーマも自動的に保証される
素晴らしいと思いませんか?ありがたいことに、RailsとSQLiteを組み合わせれば、このようなセットアップを非常に簡単に作成できます。
最後に説明した機能から説明します。これは、データベースエンジンの種類に関係なくRailsが提供する機能であり、db/migrations/
ディレクトリと/db/schema.rb
や/db/structure.sql
ファイルがもたらしてくれる価値そのものです。
データベーススキーマの管理がRailsアプリケーションのコードベースに統合され、すべてのスキーマ変更がRailsマイグレーションによって実装されているおかげで、production用スキーマをGitブランチのマージ操作にバインドするだけで、production用スキーマの予測可能性と安定性が保証されます。
率直に言うと、上の2つの機能はRailsでサポートされているどのデータベースアダプタでも可能なのですが、これから説明する方法にはSQLiteが最も適しています。
しかもこのアプローチはシンプルです。基本的に必要なのは、Gitブランチ名に紐づけられた動的なデータベース名を生成して、それを使うようRailsに伝えることだけです。Railsでは、データベースのコアな詳細を設定する/config/database.yml
ファイルを使うことで手軽に行えます。
RailsでSQLiteを使う場合、デフォルトで以下のような/config/database.yml
ファイルが生成されます。
SQLite. Versions 3.8.0 and up are supported.
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem "sqlite3"
#
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: storage/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: storage/test.sqlite3
production:
<<: *default
database: storage/production.sqlite3
シンプルでわかりやすい設定ファイルです。Railsの環境ごとにデータベースファイルが対応していることがわかります。
現在のRailsでは、すべてのデータベースファイルはstorage/
ディレクトリに保存されます(ほとんどのホスティングサービスプロバイダは、このディレクトリの内容をデプロイ間で永続化することを保証します)。また、個別のデータベースファイル名は固定されています。
ここで行いたいのは、development
環境ではデータベースファイル名に現在のGitブランチ名に基づいたファイル名を動的に設定することです。
少しググってみると、現在のGitブランチ名を取得するgit
コマンドがStackOverflowの回答で見つかります。
git branch --show-current
原注
Git 2.22(--show-current
オプションが追加された)より前のバージョンを使っている場合は、代わりにgit rev-parse --abbrev-ref HEAD
を使えます。
では、この現在のブランチ名を/config/database.yml
ファイルでどう使えばよいでしょうか?Railsでは、ERBを使えば手軽にコードを柔軟にできます。/config/database.yml
ファイルのdevelopment
セクションを以下のように置き換えます。
development:
<<: *default
database: storage/<%= `git branch --show-current`.chomp || 'development' %>.sqlite3
これで、データベースファイル名として常に固定のstorage/development.sqlite3
を使う代わりに、現在のGitブランチ名を元にデータベースファイルに動的なファイル名が指定されるようになります。この1行の変更によって、コードとスキーマを両方とも分離する単一のブランチ(Gitブランチ)を実装できました。理想的な機能その1です。
次に、Gitブランチが切り替わったら「スキーマブランチ」が自動的に切り替わるようにRailsを設定するにはどうすればよいでしょうか。
ここで上のソリューションを踏まえて、必要な要件をもう少し明確にしておく必要があります。
/config/database.yml
ファイルを変更したことで、Gitブランチを切り替えるとRailsアプリが分離されたdevelopment環境用データベースと通信可能な状態になりました。
ただし、この変更だけでは、Gitブランチを切り替えた後に、分離されたdevelopment環境用データベースを実際に利用可能になることはまだ保証されません。
- あなたの同僚がブランチを1件作成したとします。このブランチは、新しいデータベースマイグレーションを2つ追加します。
- 次にあなたはそのブランチを
git pull
して、コードレビューとローカルの手動テストを行います。 - ここであなたがローカルGitリポジトリを切り替えて同僚のブランチを初めてチェックアウトすると、動的な
/config/database.yml
設定によって、新しいSQLiteデータベースファイルがstorage/
ディレクトリに作成されます。 - しかし、この新しいSQLiteデータベースファイルの中身は空のままで、スキーマもまだ設定されていません。
では、データベースブランチが切り替わるたびに、この新しいデータベースファイルを自動的に準備するにはどうすればよいでしょうか。
ありがたいことに、これもRailsで簡単に実現できます。
Railsが提供しているActiveRecord::Tasks::DatabaseTasks
ユーティリティクラスは、「データベースやマイグレーションの管理でよく使われるタスクの背後にあるロジックをカプセル化」します。ここでは現在のニーズに合わせて.prepare_all
メソッドを利用できます。なお、このメソッドは、Rails 6で追加されたdb:prepare
コマンド(#35768)と同等です。
db:prepare
によるデータベースの準備とは、以下を行うことです。
- データベースがすでに存在する場合はマイグレーションを実行する
- データベースが存在しない場合はデータベースを作成してスキーマを読み込む
必要なのは、development環境でアプリを起動するたびにこのコマンドを実行するようにRailsに指示することだけです。これにより、動的なデータベースがRailsアプリで常に利用可能な状態になります(サーバーを実行していてもRailsコンソールを使っていても)。
development環境でアプリを起動したときにこのコマンドが実行されるようにするには、/config/environments/development.rb
ファイルに以下を追加するだけでできます。
# ブランチ固有のSQLiteデータベースをアプリケーションで使えるように準備する
config.after_initialize do
ActiveRecord::Tasks::DatabaseTasks.prepare_all
end
ここで行っているのは、アプリの初期化が完了した直後に.prepare_all
メソッドを実行するようdevelopment
環境を設定しているだけです。
このシンプルな設定を追加したことで、理想的なセットアップが実現しました。
すべてのGitブランチには、それぞれ独立したデータベースファイルがあります。Railsアプリを起動すると、そのデータベースはオンデマンドで利用できるように自動的に準備されます。また、Gitブランチのマージによってトリガーされるproduction環境へのデプロイでは、スキーマをマイグレーションで変更するだけで、予測可能かつ安定したproduction環境のスキーマが継続的に生成されるようになります。
私はこのセットアップをいくつかのRailsアプリケーションで使っていますが、本当に気に入っています!RailsとSQLiteを組み合わせることでこれを手軽に実現できる点も大好きです。
コードをたった4行(after_initialize
でインラインブロックを使えばすぐ2行に減らせます)追加するだけで、PlanetScaleのような優秀なプラットフォームと同様の(しかも重要な点も改善された)機能が手に入ったのですから。
今後数週間〜数か月ほど、このようなシンプルかつ強力な機能強化を追求していきたいと思います。本記事を気に入っていただけましたら、ぜひ@fractaledmindまでお知らせください。
概要
原著者の許諾を得て翻訳・公開いたします。
参考: Rails 8はSQLiteで大幅に強化された「個人が扱えるフレームワーク」(翻訳)|YassLab 株式会社
日本語タイトルは内容に即したものにしました。