Help us understand the problem. What is going on with this article?

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の経験年数が浅い人がいたら、この記事を共有して面倒なトラブルの発生を未然に防止しましょう!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした