【Rails】migration後にブランチを切り替えるときは、rails db:rollbackした方がいいんじゃないかと思う

所属しているコミュニティで、migrationを実行したブランチから他のブランチに切り替えたときには rails db:reset するとよい、というアイディアが出ています。
この方法を完全に否定するわけではないのですが、可能であれば別の方法を取ったほうがいいと思ったので記事を書きました。
(追記)実戦的なブランチ切替方法についてコメントをいただいたので、ページ末尾のコメントもぜひご覧ください

TL;DR

  • 開発環境のデータを守りたいなら rails db:rollback した方がいい
  • 開発環境のデータをリセットしてもいい、かつロールバックが難しいなら、 rails db:migrate:reset(rails db:reset)もやむなし
  • 困ったらとりあえずリセットすればいいと思う やっぱりできるだけロールバックしたほうがいい

rails db:migrate:reset(rails db:reset)が必要になる理由

例えば、mainブランチからAブランチを切って、migrationが必要な作業をしたとします。

  • Aブランチで orders テーブルを追加する
  • AブランチのPRを作成する
  • mainブランチに戻る
  • mainブランチからBブランチを切って作業する
  • Bブランチで employees テーブルを追加する

のように作業すると、Bブランチでのmigrationの状態はどうなるでしょうか?
結果は以下のようになります。

$ rails db:migrate:status

database: db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
...
   up     20210917144639  ********** NO FILE **********
   up     20210917145553  Create employees

Aブランチのmigrationが ********** NO FILE ********** となってBブランチに含まれてしまっていますね。

また、 schema.rb は以下のようになります。

# ...
  create_table "orders", force: :cascade do |t|
    t.integer "user_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["user_id"], name: "index_orders_on_user_id"
  end
# ...

AブランチでDBに追加された orders テーブルはBブランチでも残っているため、 schema.rborders テーブルの定義が記述されてしまっています。

この矛盾を解消する一つの手段が、 rails db:migrate:reset(rails db:reset)の実行です。
DBの構築を0からやり直し、migrationの矛盾を気にせずAブランチの変更を排除することができます。
非常に分かりやすい方法なので、手軽に実行することが可能です。
ですが、できるかぎりこの方法は避けた方がいいと思います。

rails db:migrate:reset(rails db:reset)しない方がいい理由

rails db:migrate:reset(rails db:reset)すると、開発環境のデータが吹っ飛びます。

例えば、日報を処理するアプリケーションを開発している場合、

  • 日報一覧に日報を表示するため、数十件の日報を作成する
  • 下書き状態と提出済み状態の日報をそれぞれ作成する

など、動作確認を行うために、Webアプリ上やDBで色々なデータを作成・編集したくなります。
しかし、rails db:migrate:reset(rails db:reset)はDBを削除してから再作成するので、動作確認用にコツコツ溜めたデータが消失してしまいます。

一方、rails db:rollback なら、データを維持しながら他のブランチに移動することができます。

rails db:rollback の実行の仕方

先ほどのAブランチ、Bブランチの例を流用します。
赤字がロールバックのために行う手順です。

  • Aブランチで orders テーブルを追加する
  • AブランチのPRを作成する
  • Aブランチで rails db:rollback を実行し、 orders テーブルの作成を巻き戻す
  • mainブランチに戻る
  • mainブランチからBブランチを切って作業する
  • Bブランチで employees テーブルを追加する

こうすることで、Aブランチのmigration実行がBブランチに持ち込まれないようになります。

まず、 orders テーブルを追加したあとのmigrationの状態を確認します。

$ rails db:migrate:status

database: db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
...
   up     20210917144639  Create orders

PRを作成したら、最後に実行されている Create orders をrollbackします。

$ rails db:rollback

実行したmigrationが複数ある場合は、 rails db:rollback STEP=2 などで、そのブランチで実行した全てのmigrationをrollbackします。

その後、再びmigrationの状態を確認します。

$ rails db:migrate:status

database: db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
...
  down    20210917144639  Create orders

末尾のStatusがdownになっていればOKです。
あとは git switch main --force して、Bブランチを切り出したらそちらで作業を遂行していきます。

rails db:rollback し忘れたまま別のmigrationを実行してしまった場合

Aブランチ、Bブランチの例で言うと、以下のようにしてしまった場合です。

  • Aブランチで orders テーブルを追加する
  • AブランチのPRを作成する
  • mainブランチに戻る
  • mainブランチからBブランチを切って作業する
  • Bブランチで employees テーブルを追加する

この場合は、以下のようにmigrationを巻き戻します。

  • Bブランチのmigrationファイルをコミットなりstashなりで保存する(schema.rb は含めない)
  • Bブランチで rails db:rollback を実行し、 employees テーブルの作成を巻き戻す
  • Aブランチに戻る
  • Aブランチで rails db:rollback を実行し、 orders テーブルの作成を巻き戻す
  • Bブランチに戻る
  • Bブランチで rails db:migrate を実行する

それでもどうしようもなくなったら

migrationを複数のブランチで実行しているうちに、どうにも rails db:rollback できなくなってしまう場合もあります。
その場合は rails db:migrate:reset(rails db:reset) を実行しましょう。
seedが整備されているプロジェクトであれば、必要最低限の開発環境用データは再生成されます。
rails db:migrate:reset を実行した場合は、そのあとに rails db:seed を実行する必要があります

余談: rails db:migrate:resetrails db:reset のどちらを選ぶか

この二つには以下のような差異があります。

  • rails db:migrate:reset: migrationファイルからDBを構築する。 rails db:seed を実行しない
  • rails db:reset: schema.rb からDBを構築する。 rails db:seed を実行する

冒頭で示したような例に陥ると、 schema.rb は別ブランチの変更を含んでしまっている可能性があります。
そのため、基本的には rails db:migrate:reset (+ rails db:seed) を実行するのが良いと思います。