みなさん Rails でマイグレーションを使ってると思います(大きなところ以外は?)
デプロイでマイグレーションが失敗することはありませんか?
マイグレーションが失敗すると, 場合によっては DB をバックアップから復元する必要があったりします.
安全な migration ファイルをつくって気楽にデプロイしましょう.
どういうときにマイグレーションが失敗するのか
- fk が絡んだとき
- uniq インデックスを付けたいのにすでに重複しているとき
- 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_column
と add_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 で本番データと同じものを用意してマイグレーションしてみるとか (ただし, それが許されるなら)
そもそもマイグレーション使わんほうがいいって話もありますがとはいえ