Edited at

GitHubにpushしたmigrationファイルは安易に修正してはいけません

More than 3 years have passed since last update.


はじめに

人間は誰しも間違いを犯します。

「恥ずかしい間違いはすぐに修正してなかったことにしたい」と考えるのは当然の心理です。

幸いなことに、プログラム中のささいなtypoであれば、ささっと修正してコミットすれば、あたかも何もなかったかのように過去の間違いをかき消すことができます。

が!

Railsアプリケーションの場合、migrationファイルだけは安易に修正してはいけません。

この記事ではその理由と、正しい修正の手順を紹介します。


問題が起きるシナリオ:花子さんはdb:migrateできない

あるブログシステムにはUserテーブルがあります。

太郎さんはここに生年月日を保存するカラムを追加しました。

class AddBarthdayToUsers < ActiveRecord::Migration

def change
add_column :users, :barthday, :date
end
end

太郎さんはdb:migrateを実行し、コードに加えた変更をコミットしたあと、コードをGitHubにpushしました。

同じ開発チームの花子さんは最新のコードをGitHubからpullしました。

新しくmigrationファイルが追加されているので、db:migrateを実行します。

その頃、太郎さんは自分の間違いに気づきました。

「しまった、barthdayじゃなくてbirthdayだった!!」

太郎さんはあわててdb:rollbackを実行しました。

それからmigrationファイルを次のように修正し、もう一度db:migrateを実行しました。

class AddBarthdayToUsers < ActiveRecord::Migration

def change
add_column :users, :birthday, :date
end
end

「ふう、これで大丈夫だ」

安心した太郎さんは次の開発ステップに進みます。

生年月日は頻繁に検索される項目だったのでインデックスを付けることにしました。

class AddIndexOnBirthdayToUsers < ActiveRecord::Migration

def change
add_index :users, :birthday
end
end

db:migrateを実行したあと、変更をコミットし、GitHubにコードをpushしました。

太郎さんから「さっきコードをpushしたよ」と連絡を受けた花子さんは、もう一度GitHubからコードをpullします。

今回もmigrationファイルが追加されているので、さっそくdb:migrateを実行したところ・・・

rake aborted!

StandardError: An error has occurred, this and all later migrations canceled:

SQLite3::SQLException: table users has no column named birthday: CREATE INDEX "index_users_on_birthday" ON "users" ("birthday")/.../db/migrate/20151022231855_add_index_on_birthday_to_users.rb:3:in `change'
== 20151022231855 AddIndexOnBirthdayToUsers: migrating ==================================
-- add_index(:users, :birthday)
-e:1:in `<main>'
ActiveRecord::StatementInvalid: SQLite3::SQLException: table users has no column named birthday: CREATE INDEX "index_users_on_birthday" ON "users" ("birthday")
/.../db/migrate/20151022231855_add_index_on_birthday_to_users.rb:3:in `change'
-e:1:in `<main>'
SQLite3::SQLException: table users has no column named birthday
/.../db/migrate/20151022231855_add_index_on_birthday_to_users.rb:3:in `change'
-e:1:in `<main>'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)

こんなエラーが発生してしまいました。

花子さんはなんでエラーが発生したのかわかりません。

とりあえず、花子さんは太郎くんのデスクへ出向き、彼の胸ぐらをつかんで「お前、いったいさっき何やってん?ああん!?」と小一時間問い詰めましたとさ。

おしまい。


問題が起きた理由:無責任にスキーマの差異を作ってしまったから

さて、この問題が起きた理由は、簡単にいうと太郎さんと花子さんのデータベースの状態(スキーマ)に差異が発生したためです。

時系列に太郎さんと花子さんの状況をまとめるとこうなります。

Action
Migration
太郎さんのDB
花子さんのDB
ポイント

太郎さんがmigrationを作って実行
add_column :users, :barthday, :date
barthday
-
 

太郎さんがGitHubにpush
 
barthday
-
 

花子さんがGitHubからpull
 
barthday
-
 

花子さんがmigration実行
add_column :users, :barthday, :date
barthday
barthday
 

太郎さんがtypoに気づく
 
barthday
barthday
 

太郎さんがdb:rollback
 
-
barthday
 

太郎さんがmigrationを修正して実行
add_column :users, :birthday, :date
birthday
barthday
ここで差異が発生

太郎さんが新しいmigrationを実行
add_index :users, :birthday
birthday
barthday
 

太郎さんがGitHubにpush
 
birthday
barthday
 

花子さんがGitHubからpull
 
birthday
barthday
 

花子さんがmigration実行
add_index :users, :birthday
birthday
barthday
エラー発生!!

「ポイント」の列を見てもらえればわかると思いますが、太郎さんが過去のmigrationを修正して実行したために、花子さんのDBと差異が発生しています。

そして、最後にadd_indexのmigrationは「太郎さんの環境では動くが、花子さんの環境では動かないmigrationファイル」になっています。

では、太郎さんはいったいどうすれば良かったのでしょうか?


正しい手順:問題を修正するmigrationを新たに作る

正しい手順は「過去に作ったmigrationファイルを修正するな」の反対(?)で、「修正したいなら新しくmigrationを作れ」です。

つまり、太郎さんは次のようなmigrationファイルを作ればよかった、ということになります。

class RenameBarthdayToBirthdayInUsers < ActiveRecord::Migration

def change
rename_column :users, :barthday, :birthday
end
end

このmigrationを実行したあとにadd_indexすれば、花子さんの環境でも問題は起きません。

正しい手順で実行した場合のシナリオを時系列にまとめてみましょう。

Action
Migration
太郎さんのDB
花子さんのDB
ポイント

太郎さんがmigrationを作って実行
add_column :users, :barthday, :date
barthday
-
 

太郎さんがGitHubにpush
 
barthday
-
 

花子さんがGitHubからpull
 
barthday
-
 

花子さんがmigration実行
add_column :users, :barthday, :date
barthday
barthday
 

太郎さんがtypoに気づく
 
barthday
barthday
 

太郎さんが修正用のmigrationを作って実行
rename_column :users, :barthday, :birthday
birthday
barthday
新しいmigrationで修正

太郎さんが新しいmigrationを実行
add_index :users, :birthday
birthday
barthday
 

太郎さんがGitHubにpush
 
birthday
barthday
 

花子さんがGitHubからpull
 
birthday
barthday
 

花子さんがmigration実行
rename_column :users, :barthday, :birthday
birthday
birthday
カラム名が同期する

 
add_index :users, :birthday
birthday
birthday
エラーは起きない

ご覧の通り、リネーム用のmigrationを作ったので、花子さんのDBでもカラム名が修正され、それからadd_indexが実行されています。


注意:安易な一括置換も危険です!

typoを修正したり、リファクタリングしたりするために、プロジェクト全体のメソッドや変数名を一括置換することもあると思います。

この場合も何も考えずに、

「一気に全部置換!テストもパス!コミット!push!はい完了!(ッターン!!)」

とやってしまうのは危険です。

なぜなら一括置換したファイルの中にmigrationファイルが含まれている可能性があるからです。

一括置換する場合は、ちょっと面倒でも次のようにやった方が安全に変更できます。


  1. 「すべてを置換」ではなく、「置換、次へ」を1件ずつ繰り返す。

  2. migrationファイルや schema.rb に遭遇したら置換せずに次に進む。

  3. コミットする前にdiffを確認する。

  4. 変更されたファイルにmigrationファイルや schema.rb が含まれていなければコミットを実行する。

特に一括置換をする、しないにかかわらず、予期しないトラブルを防ぐために「コミット前のdiff確認(セルフレビューの実施)」は習慣化しておく方がいいと思います。


まとめ:デプロイ時に発生したらもっと大変なので、未然に防止しよう

というわけで、この記事では過去に作成したmigrationファイルを修正することの危険性と、そのトラブルを防ぐための正しい手順について説明しました。

ここで紹介したシナリオは「花子さんが困る」ケースでしたが、「花子さんのDB」は「本番環境のDB」に置き換えることもできます。

そうすると、コードのデプロイ時に問題が発覚し、予定通りに機能をリリースできなくなってしまいます。

リリースを急いであわてて問題を修正すると、さらに別の問題が発生してもはや目も当てられない状況に・・・なんていうことも十分ありえます。

Rails開発を長くやってると、migration絡みで何度か痛い目に遭って「migrationの扱いは気をつけないと危険」ということが感覚的に身に付いてきます。

しかし、経験が浅いと、他のコードと同じ感覚でmigrationファイルもさらっと書き換えてしまう人が多いようです。

もし開発チーム内にRailsの経験年数が浅い人がいたら、この記事を共有して面倒なトラブルの発生を未然に防止しましょう!