TL;DR:
RAILS_ENV=test rails db:migrate:reset db:seed
実行したときにreset_column_information
があると意図しないDBスキーマがRailsによってキャッシュがされてしまい、NoMethodError
が発生した。
現象:
RAILS_ENV=test rails db:migrate:reset
自体は成功しており、DBには期待したテーブル構成が構築されていることを確認できた。
ところがそのあとに実行されるdb:seed
が期待した挙動にならずエラーが発生する。
RAILS_ENV=test rails db:migrate:reset
とRAILS_ENV=test rails db:seed
を別々に実行すると成功するという謎挙動に遭遇した。
原因:
まだ具体的にどこのコードが影響してそうなったのか?がわかっていないのだが少なくとも原因の主な理由は reset_column_information
にあることが判明した。
発生する条件としては↓のような状態となる。
-
rails db:migrate:reset db:seed
をワンライナーで実行する - マイグレーションファイル内で
reset_column_information
を実行している -
RAILS_ENV
がtest
-
development
だと発生しない、理由は後述する
-
再現:
↓な感じのマイグレーションファイルを用意する。
class RenameCancelledToStatus < ActiveRecord::Migration[5.1]
def up
rename_column :model, :cancelled, :ok
Model.reset_column_information
Model.find_each do |model|
model.ok = !model.ok
model.save!
end
end
def down
rename_column :model, :ok, :cancelled
Model.reset_column_information
Model.find_each do |model|
model.cancelled = !model.cancelled
model.save!
end
end
end
内容としてはイケてないカラムcancelled
さんをok
さんにリネームするというもの。
(カラム名に対して突っ込んではいけない、わかりやすくするためにあえて名付けているのだ。)
Model
のカラムをリネームしているのでそのままだとリネーム前のカラムを参照してしまうのでreset_column_information
でモデルデータを読み込み直している。
一見?問題なさそうに見える。
実際RAILS_ENV=development rails db:migrate:reset db:seed
したところ、正常に完了する。
DBに入っている値もきちんと反転した状態で入っていたので意図した値が登録されているため、問題ないと思っていた、このときまでは。
CIサーバーからFailの通知が来た
一応手元の環境で確認が出来たのでgit push
をしてCIを回したところテストがFailしたという通知が届いた。
CIをチェックするとDBの初期化、つまりRAILS_ENV=test rails db:migrate:reset db:seed
でNoMethodError
が発生して失敗をしている。
何故 db:seed
でエラーになる?とこの時点でかなり頭を抱えそうだなと感じていたがだいたいこの手の嫌な予感は外れない法則が的中した。
エラーメッセージをみると NoMethodError: undefined method 'ok='
が発生しており、現象としてはdb:seed
を実行してmodel.ok
というカラムにデータを突っ込もうとしたらエラーという状況になっているということまではわかった。
はて?migration時にはエラーにならないのに何故 db:seed
でエラーになるのか?がよくわからなくて途方に暮れた。
ちなみに当初 'ok='
というメソッドでエラーになってしまっているので何かしらのタイポをしたのだと思っていた。
とりあえずいろいろ調査する
ちょっと原因の想定がつかないな…と思いながらもまずは手元の環境でログを出して調べようとrails db:migrate:reset db:seed
を実行する→何故か完了するという現象に遭遇してしまう。
(このとき手元の環境はRAILS_ENV=development
だった)
この時点で完全に謎の迷宮に放り込まれたが、StackOverflowやGithubのissueなどをみたり、いろいろ手元で試した結果「これはそもそもRailsがModelを起動時にキャッシュしているのでは?」という仮説が浮かび上がった。
その後同僚が協力して調べてくれた内容を共有してくれて恐らくキャッシュのせいだろうと結論づけた。
解決方法:
Railsのキャッシュ周りがどうなっているのか?を調べる時間的余裕がこのときあまりなかった。
そのうえでどう解決するかを考えたところ、そもそも RAILS_ENV=test rails db:migrate:reset db:seed
を行うときにだけ発生するのでとりあえずCIでワンライナーで実行しているところを分割するという対策にしようということになった。
本来はきちんと解決すべき問題なのかもしれないがそもそもの影響範囲がテストのときだけ(開発環境はRAILS_ENV=development
なので影響がない)ということで少なくともいまのところ問題にはなっていない。
まとめ:
今回の想定外だった箇所としては2つあると考えている。
-
rails db:migrate:reset db:seed
がrails db:migrate:reset && rails db:seed
と等価だと思っていたこと。- 実際にはキャッシュの生成タイミングなどに微妙な差異があった。
-
db:seed
実行時に最新のモデルが読み込まれていると思い込んでいたこと- ワンライナーで実行したときにも
db:migrate:reset
後にリロードされると思っていたが実際にはそうでなかった- 今にして思えばリロードしてしまうのはコストが高い気がするので仕方ないように思う。
- ワンライナーで実行したときにも
ということで根本的な解決ではないが対処方法としてはまあまあ妥当でかつ簡単に対応出来た点は良かったかもしれない。
恐らくもっと正しい対処方法があると思うので知ってる方がいたら教えてください。