LoginSignup
2
0

More than 5 years have passed since last update.

安全な! migration ファイルの作り方

Last updated at Posted at 2018-09-03

みなさん Rails でマイグレーションを使ってると思います(大きなところ以外は?)

デプロイでマイグレーションが失敗することはありませんか?
マイグレーションが失敗すると, 場合によっては DB をバックアップから復元する必要があったりします.

安全な migration ファイルをつくって気楽にデプロイしましょう.

どういうときにマイグレーションが失敗するのか

  1. fk が絡んだとき
  2. uniq インデックスを付けたいのにすでに重複しているとき
  3. NOT NULL 制約を付けたいのに, NULL が入っているとき

などなど

本番デプロイでマイグレーションがコケるとどうなる?

最初の部分で失敗していればいいのですが, 途中で失敗すると中途半端なDB になってしまいます.
再度デプロイしようとしても, また最初からマイグレーションが実行されるので今度は最初の部分で失敗します.
なので, バックアップから復元して元の状態に戻したりなどの対応が必要ですが,
その間アプリケーションが正常な状態ではなくなるので, ご不便です

class ChangeColumnsToBooks < ActiveRecord::Migration[5.0]
  def change
    add_column :books, :name, :string
    add_index :books, :isin, unique: true # <= ここでコケると面倒
  end
end

解決策

migration の単位を小さくする

上の例ですと add_columnadd_index を別の migration ファイルにしてしまう

class AddNameToBooks < ActiveRecord::Migration[5.0]
  def change
    add_column :books, :name, :string
  end
end


class AddIsinIndexToBooks < ActiveRecord::Migration[5.0]
    add_index :books, :isin, unique: true # <= ここでコケても次回またここからはじまる
  end
end

とはいえ再デプロイまでアプリケーションが動いていると, アプリケーションコードと DB の schema が合わない状態が続くので良くない

レコード をいじくる(本題)

class AddIsinIndexToBooks < ActiveRecord::Migration[5.0]
  def change
    Book.order(isin: :asc).each_cons(2) do |first, second|
      first.destroy if first.isin == second
    end
    add_index :books, :isin, unique: true
  end
end

これでとりあえず books.isin が重複したカラムはなさそう
ただし, もし追加した部分のコードでは不十分だった場合は?
デプロイでそれがわかっても後の祭り

いじくった後にチェックする => だめならロールバック

class AddIsinIndexToBooks < ActiveRecord::Migration[5.0]
  class まだ重複しているレコードがあるよエラー < StandardError; end
  def change
    ActiveRecord::Base.transaction do
      Book.each_cons(2) do |first, second| 
        first.destroy if first.isin == second
      end
      raise まだ重複しているレコードがあるよエラー if Book.pluck(:isin).uniq.count != Book.count
    end
    add_index :books, :isin, unique: true
  end
end

これなら, レコードをいじくる部分に不備があったとしても元の状態にロールバックしてくれます

将来 Book の実装が変化したとき

class AddIsinIndexToBooks < ActiveRecord::Migration[5.0]
  class Book < ActiveRecord::Base; end # <= アプリケーションの Book の実装に依存しないように
  def change
    Book.order(isin: :asc).each_cons(2) do |first, second| # <= ここで使われるのは `AddIsinIndexToBooks::Book` になる
      first.destroy if first.isin == second
    end
    add_index :books, :isin, unique: true
  end
end

マイグレーションでモデルとかを使う場合はマイグレーション用にクラスを作って置きましょう

after_destory とかのコールバックが追加されたりする場合にも対応できます
マイグレーションはなるべくアプリケーションコードに依存しないようにしときましょう

結論

あとは, staging で本番データと同じものを用意してマイグレーションしてみるとか (ただし, それが許されるなら)
そもそもマイグレーション使わんほうがいいって話もありますがとはいえ

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0