はじめに
これは migrationファイルのサンプルだが、何が悪いかわかるだろうか。
class AddNameToUser < ActiveRecord::Migration
def change
add_column :users, :name, :string
User.find_each do |u|
u.name = u.email.split("@")[0]
end
end
end
このファイルを使ってマイグレーションした時にはエラーは発生しない。しかし新たな環境にデプロイする時に謎のエラーが発生するかもしれない。
原因
ActiveRecord::Baseを継承したクラスはインスタンスを作成するタイミングで、データベーススキーマを確認する。
上記の例では、find_eachの中でusersテーブルのスキーマを確認し、Userクラスに保存しておく。
このスキーマ情報は各モデルクラスにキャッシュされる。インスタンス化のたびにスキーマを参照するわけにはいかないので当然だろう。
しかしマイグレーション時には、このキャッシュが悪さをする。
さらにユーザにnick_nameというフィールドを追加することにした。
class AddNickNameToUser < ActiveRecord::Migration
def change
add_column :users, :nick_name, :string
User.find_each do |u|
u.nick_name = u.name
end
end
end
これもおかしなプログラムではない。しかし2つのマイグレーションを連続で実行するとエラーが発生する。
最初のAddNameToUserでスキーマがキャッシュされる。この時点ではnick_nameというフィールドは無い。
次のAddNickNameToUserでスキーマはキャッシュされており、読み込まない。この結果Userクラスはnick_nameフィールドを持たないということになり、代入時にエラーが出る。
対応
実際のところdb:migrateが失敗に終わってもAddNameToUserの直前まではマイグレートが完了している。つまりエラーがでなくなるまでrake db:migrateを繰り返し実行すれば問題は回避する。いわゆる「運用でカバー」だ。
しかし問題は解決しておこう。この問題はスキーマがキャッシュされるのが原因なので、キャッシュさせなければよい。つまり、以下のようなコードを書けば良い。
class AddNameToUser < ActiveRecord::Migration
# キャッシュされるクラス
class User < ActiveRecord::Base; end
def change
add_column :users, :name, :string
User.find_each do |u|
u.name = u.email.split("@")[0]
end
end
end
こうすることで、スキーマがキャッシュされるのは AddNameToUser::User であり、他のマイグレーションには影響を与えない。複数のテーブルを扱うのであれば、その数だけ定義し、has_manyなどのリレーションをはること。
残念ながらmodelのロジックを流用するには面倒だが、定数の利用くらいならできる。
class AddNameToUser < ActiveRecord::Migration
class User < ActiveRecord::Base; end
def change
add_column :users, :name, :string
User.find_each do |u|
u.name = ::User.DEFAULT_NAME
end
end
end
まとめ
運用でカバーできるので頑張らなくても良いかもしれない。
しかし、綺麗に一発でマイグレーションできたほうが気持ちが良いし、
bash等のスクリプトで実行する場合 while を書く必要が出たりで面倒なので、そういうもんだと思って上記の方法を取るのが良いように思う。