【ITニュース解説】Como aceleramos em 90% a execução das nossas migrações em Rails
2025年09月04日に「Dev.to」が公開したITニュース「Como aceleramos em 90% a execução das nossas migrações em Rails」について初心者にもわかりやすいように丁寧に解説しています。
ITニュース概要
Railsのデータベース変更(マイグレーション)は、数が増えると実行に時間がかかり開発効率が落ちる問題があった。そこで、古いマイグレーションを一つにまとめたSQLファイルを作成し、データ生成部分は別のファイルに移すことで、新しい環境でのデータベース構築時間を90%短縮した。
ITニュース解説
システム開発において、データベースの構造(スキーマ)を管理することは非常に重要です。特にRailsのようなフレームワークでは、「マイグレーション」という機能を使って、データベースのテーブル作成やカラムの追加・変更といった操作をコードとして管理します。これにより、複数の開発者が協力して開発を進めたり、アプリケーションのバージョンアップに合わせてデータベース構造を安全に変更したりできます。
しかし、このマイグレーションにはプロジェクトの規模が大きくなるにつれて問題が生じます。開発期間が長くなり、アプリケーションの機能が増えるほど、マイグレーションファイルの数も膨大になっていきます。一つ一つのマイグレーションはデータベースに変更を加えるための指示ですが、これらが何百、何千と増えると、新しく開発環境を構築したり、別の機能開発のためにデータベースをゼロから再構築したりする際に、すべてのマイグレーションを順に実行するのに非常に長い時間がかかってしまうのです。特に、私たちは6年分のマイグレーションを抱えており、新しい環境を作るたびにデータベースを再構築するのに大きな負担を感じていました。
この遅さの原因の一つは、Railsのデフォルト設定にあります。通常、各マイグレーションは「トランザクション」と呼ばれるデータベースの一連の操作として実行されます。トランザクションは、途中でエラーが発生した場合にすべての変更を取り消し、データベースを元の状態に戻すための安全な仕組みです。本番環境でデータベースの構造を途中で壊してしまうような事態を避けるためには非常に有効ですが、何百ものマイグレーションがそれぞれ個別のトランザクションとして順番に実行されると、そのたびにデータベースとのやり取りが発生し、全体の処理速度が著しく低下します。
また、Railsにはschema:loadというコマンドがあります。これは、現在のデータベース構造をRubyコードで表現したschema.rbというファイルを読み込むことで、一瞬でデータベース構造を構築できる非常に高速な方法です。しかし、この方法は「ビュー」と呼ばれるデータベースの仮想テーブルや、マイグレーションファイルの中にRubyコードとして書かれたデータ投入(例えば、User.create!のように特定のデータをデータベースに直接作成する処理)には対応していません。そのため、本番環境やデータの整合性が重要な環境ではschema:loadだけでは不十分で、結局すべてのマイグレーションを実行する必要がありました。
このような状況を解決するため、私たちは新しいアプローチを検討しました。それは、過去の膨大なマイグレーションを一括で処理できる「ベースライン」となるSQLファイルを作成し、それを直接データベースに読み込むことで、古いマイグレーションの実行時間を大幅に短縮するというものです。そして、このベースライン以降の新しいマイグレーションだけは、従来通り実行するようにします。
この解決策を実装する上で、特に注意が必要だったのが、マイグレーションの中に含まれる「データ投入」のコードでした。先述のschema.rbや、データベース固有の構造まで含んだstructure.sqlというファイルは、データベースの構造(テーブルやカラム、インデックスなど)は記録しますが、マイグレーション中にRubyコードで作成された具体的なデータ(例:User.create!(name: 'John'))は含みません。もしこれらのデータ投入を無視してベースラインを構築してしまうと、新しい環境で必要なデータが不足する事態が発生してしまいます。
そこで私たちは、どのマイグレーションがデータベースにデータを挿入したり更新したりしているかを自動で特定する仕組みを一時的に導入しました。具体的には、マイグレーションが実行される際に、裏側で発行されるSQL文(INSERTやUPDATEなどのデータ操作命令)をログとして記録するシンプルなツールを作成し、これを使って該当するマイグレーションを洗い出しました。数千あったマイグレーションの中から、実際にデータ操作を行っているものが70程度まで絞り込まれたため、それらを一つ一つ確認し、本来「初期データ」として扱われるべきものはdb/seeds.rbという別のファイルに移動させました。seeds.rbは新しい環境で初期データを投入するための標準的なRailsの機能です。
次に、ベースラインとなるSQLファイルの生成に取りかかりました。私たちは2024年までのすべてのマイグレーションをベースラインとすることにしました。まず、Railsの設定でデータベースのスキーマ情報をSQLファイル形式で出力するように変更します(config.active_record.schema_format = :sql)。そして、2025年以降の新しいマイグレーションファイルを一時的に別のフォルダに移動させた後、データベースを一度完全に削除して再構築します。この時、Railsは2024年までのマイグレーションを実行し、その結果として現在のデータベース構造がstructure.sqlというファイルに出力されます。このファイルをstructure_baseline.sqlという名前で保存し、これを新しい環境で一括で読み込む「ベースライン」とします。その後、ベースライン以前のマイグレーションファイルは削除し、移動させていた2025年以降のマイグレーションファイルを元の場所に戻します。
このstructure_baseline.sqlを新しい環境で読み込むための特別なマイグレーションを作成しました。このマイグレーションは、ActiveRecord::Base.connection.execute(File.read(Rails.root.join('db/structure_baseline.sql')))というコードで、保存しておいたSQLファイルを直接データベースに実行します。さらに、このマイグレーションにはdisable_ddl_transaction!という設定を加えました。これは、このマイグレーション内で行われるデータベース操作をトランザクションの対象外とすることで、SQLファイルの読み込み処理をさらに高速化するものです。ただし、これにより途中でエラーが発生しても元に戻せないため、主に新しい環境の初期構築時にのみ適用し、既存の環境ではこのマイグレーションが実行されないよう、schema_migrationsテーブルに手動で実行済みの記録を挿入する対応も行いました。
最後に、生成されたstructure_baseline.sqlファイルを一部修正する必要がありました。Railsはar_internal_metadataやschema_migrationsといった内部管理用のテーブルを自動的に作成しますが、structure_baseline.sqlにもこれらのテーブルの作成やインデックスの定義が含まれていると、既存の環境で再度マイグレーションを実行する際にエラーとなることがあります。そのため、SQLファイル内のCREATE TABLEやADD CONSTRAINTの記述にIF NOT EXISTSという条件を追加し、もし既に存在していれば作成や追加を行わないように変更しました。また、データベースの拡張機能に対するコメント行が、権限が不足しているユーザーで実行するとエラーになる場合があったため、その行を削除する対応も行いました。
このような一連の対応を行うことで、私たちのマイグレーションの実行時間は大幅に改善され、およそ90%の高速化を達成することができました。これにより、新しい開発環境の構築やブランチ切り替え時のデータベース再構築にかかる時間が劇的に短縮され、開発効率が向上しました。
ただし、この解決策にはいくつか注意点があります。第一に、structure_baseline.sqlは特定のデータベース(今回の例ではPostgreSQL)に特化したSQLコードであるため、Railsが提供する「データベースの種類に依存しない」というschema.rbの大きな利点が失われます。複数の種類のデータベースを使用する環境では、このアプローチは適していません。第二に、この方法はRailsの公式なマイグレーションの最適化手法ではないため、マイグレーション履歴の管理や、過去の変更を元に戻す「ロールバック」の容易さが損なわれる可能性があります。したがって、もしマイグレーションの遅延が大きな問題となっていないのであれば、この複雑な最適化を導入するメリットは少なく、標準的なRailsの運用を続ける方が安全であると言えるでしょう。