はじめに
人間は誰しも間違いを犯します。
「恥ずかしい間違いはすぐに修正してなかったことにしたい」と考えるのは当然の心理です。
幸いなことに、プログラム中のささいな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件ずつ繰り返す。
- migrationファイルや
schema.rb
に遭遇したら置換せずに次に進む。 - コミットする前にdiffを確認する。
- 変更されたファイルにmigrationファイルや
schema.rb
が含まれていなければコミットを実行する。
特に一括置換をする、しないにかかわらず、予期しないトラブルを防ぐために「コミット前のdiff確認(セルフレビューの実施)」は習慣化しておく方がいいと思います。
まとめ:デプロイ時に発生したらもっと大変なので、未然に防止しよう
というわけで、この記事では過去に作成したmigrationファイルを修正することの危険性と、そのトラブルを防ぐための正しい手順について説明しました。
ここで紹介したシナリオは「花子さんが困る」ケースでしたが、「花子さんのDB」は「本番環境のDB」に置き換えることもできます。
そうすると、コードのデプロイ時に問題が発覚し、予定通りに機能をリリースできなくなってしまいます。
リリースを急いであわてて問題を修正すると、さらに別の問題が発生してもはや目も当てられない状況に・・・なんていうことも十分ありえます。
Rails開発を長くやってると、migration絡みで何度か痛い目に遭って「migrationの扱いは気をつけないと危険」ということが感覚的に身に付いてきます。
しかし、経験が浅いと、他のコードと同じ感覚でmigrationファイルもさらっと書き換えてしまう人が多いようです。
もし開発チーム内にRailsの経験年数が浅い人がいたら、この記事を共有して面倒なトラブルの発生を未然に防止しましょう!