何があったか
ある時に実装に必要な修正を行うために既存のテーブルの定義を返る必要があり、別の方がマイグレーションを行った。
その後APIの修正のためにpytestの修正を行い、テストを実行しようとしたところテスト用のデータベースをマイグレーションするところで引っかかり実行できなくなってしまった。
調査をすると今回マイグレーションで修正したテーブルはそのテーブル作成時のマイグレーションでloaddata
を使ってjsonから初期データを登録していたことがわかった。
定義を変更したマイグレーションをAとし、テーブル作成時のマイグレーションをBとするとテスト用のデータベースのマイグレーションはBのタイミングで止まっていた。
原因
まずpytestでは特に設定等していない限り、テストを実行すると必要なテーブルを作成するためにDjangoのmigrationファイルに基づいて頭から順次migrateを行う。
しかしソースコードを読むと、loaddata
の実態はfixture配下の指定されたjsonを読んでそれをDeserializeし、対象のmodelにマッピングするような処理になっていた。
つまり、pytestはマイグレーションBを行おうとするが対象のmodelはすでにマイグレーションAの仕様になっているのでカラムやデータが合わないからloaddata
できないよと怒られるということだった。
では、読み込むjsonの方をmodelに合わせて修正したり、loaddata
のオプションでignorenonexistent
というものがありそれを使うと作成時から定義が変わった部分を無視(空文字を入れる)できるというものがあるので、それでいいのではとなるがそうすると今度はマイグレーションBの時点のテーブルの定義と外れてしまうのでエラーは解消できなかった。
対策
マイグレーションBで行っているloaddata
をマイグレーションAに移して、migration.RunSQL
で対象のテーブルのデータを削除した後に実行するように修正する。
あるいはその一連のDelete Insertの処理を新しいマイグレーションファイルを作成してそこに書く。
そうすると
マイグレーションBでテーブル作成(中身は空) → マイグレーションAで定義変更 → 定義変更に合わせたデータをDelete Insert
という動きになるので解決はする。
ただし、これだとローカル環境でのmigrateは容易に済むがステージングや本番環境など該当のテーブルのデータが初期データではない場合はリリースの際にデータが吹っ飛んでしまうので予めバックアップを取り、改めてそれをDelete Insertしないといけない。
または元データが別のリソース(Redshiftとかsnowflakeとか)にある場合は同様に改めてデータを取得したのちそれをDelete Insertする必要がある。
あとがき
ちなみにpytest(というかpytest-django)にはDjango側のマイグレーションを無視し、原因のmodelを参照してそこからテストテーブルを作成する--no-migrations
というオプションがあるが、マイグレーションがあるのに無視してしまってはテストとしてどうなのよということで採用はしなかった。
また少し古いStackOverFlowになるけれどもこういう例もある。
こっちのほうがいいような気もするけれど一見だと何やってるか分かりづらいのと、結局マイグレーションを弄るし、今回はfixtureも修正して初期データの中身も増やす必要があるのでそれなら新しいマイグレーションにloaddataを移動させてDelete Insertするほうがいいかも。
マイグレーションA自体はすでにレビュー済みでMergeされていたし、pytestのマイグレーションは必ず1からやるので当然テーブルの作成かつ旧仕様の初期データを追加 → その後定義変更のマイグレーションが普通に行われるものだと思っていたが飛んだところに罠が潜んでいたなと。
エラーが起きたときに何が悪いのかイマイチ掴めず、先輩に指導頂きながらようやく原因がわかったときは軽いアハ体験だった。
マイグレーションAのタイミングでpytestを実行したり、或いは担当者が気を回せていればもう少しうまいことやれたのかもしれない。
もしこれを読まれた方でもっと良い方法があれば教えていただけると幸いです。