はじめに
これは 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
を書く必要が出たりで面倒なので、そういうもんだと思って上記の方法を取るのが良いように思う。