【Django】マイグレーション時のエラー OperationalError: no such table を解決する
概要
Djangoを使ってちょっとしたアプリを作っていた時に、マイグレーション実行時以下のようなエラーが出てきた。
django.db.utils.OperationalError: no such table: tests_test1
エラーの原因とそれに対する解決方法をメモしておく。
バージョン情報
Django 3.1.4 を使用
事象の再現
以下のようなオペレーションを行うことによって、事象を再現することが可能。
詳細に1ステップずつ見ていく。
models.py に以下のように、2つのカラムを持つ Test というモデルを定義する。
from django.db import models class Test(models.Model): column1 = models.CharField(max_length=10) column2 = models.CharField(max_length=20)
マイグレーションを作成・実行する。
(py39) C:\Users\XXX\TestProject>python manage.py makemigrations Migrations for 'tests': tests\migrations\0003_test.py - Create model Test (py39) C:\Users\XXX\TestProject>python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions, tests Running migrations: Applying tests.0003_test... OK
次に、上記のマイグレーションで作成したテーブルを、SQLiteのコマンドラインツールから直接削除する。
(py39) C:\Users\XXX\TestProject>sqlite3 db.sqlite3 SQLite version 3.33.0 2020-08-14 13:23:32 Enter ".help" for usage hints. sqlite> .tables auth_group django_admin_log auth_group_permissions django_content_type auth_permission django_migrations auth_user django_session auth_user_groups tests_test auth_user_user_permissions tests_test1 sqlite> DROP TABLE tests_test; sqlite> .tables auth_group django_admin_log auth_group_permissions django_content_type auth_permission django_migrations auth_user django_session auth_user_groups tests_test1 auth_user_user_permissions
DROP TABLEの実施前後に実施した .tables コマンドの結果から、tests_test というテーブルが削除されていることが分かる。
次に、 models.py 上で削除されたテーブルに対する変更を加える。
from django.db import models class Test(models.Model): column1 = models.CharField(max_length=10) column2 = models.CharField(max_length=20) column3 = models.CharField(max_length=30) # 追加
ここでは新たに column3 というカラムを追加している。
この状態で再度マイグレーションを作成すると、以下の警告が出力される。
(py39) C:\Users\XXX\TestProject>python manage.py makemigrations You are trying to add a non-nullable field 'column3' to test without a default; we can't do that (the database needs something to populate existing rows). Please select a fix: 1) Provide a one-off default now (will be set on all existing rows with a null value for this column) 2) Quit, and let me add a default in models.py Select an option:
ここでは1を選択し、デフォルトの値として適当な値(1など)を入力して処理を進めたうえ、マイグレーションを実行すると、以下のエラーが得られる。
(py39) C:\Users\XXX\TestProject>python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions, tests Running migrations: ~~~ (中略) ~~~ django.db.utils.OperationalError: no such table: tests_test
エラーの原因
エラーの原因はエラーメッセージにもある通り「マイグレーションで変更を加えようとしたテーブルが存在しないこと」である。
事象の再現に記載したように、Djangoのマイグレーション以外の方法でテーブルを消去すると、Djangoのマイグレーションで管理しているDBの情報と実際のDBの状態に不整合が生じ、上記のエラーが発生する。
解決方法
エラーの原因はテーブルが存在しないことなので、テーブルを再度作成することで解決しそうである。 テーブルを復活させるには、以下のような方法が考えられる。
ここでは1の方法を記載する。
まずは現在のマイグレーションの状態を、showmigrations というコマンドで確認する。
(py39) C:\Users\XXX\TestProject>python manage.py showmigrations admin [X] 0001_initial [X] 0002_logentry_remove_auto_add ~~~ (中略) ~~~ sessions [X] 0001_initial tests [X] 0001_initial [X] 0002_test1_column3 [X] 0003_test [ ] 0004_test_column3
今回対象のアプリである tests の欄に注目すると、
- 0003_test は[X](反映済み)
- 0004_test_column3 は[ ](未反映)
ということが分かる。言い換えると、
という状態であるといえる。
従って、この状態のままマイグレーションを実施したとしても、testテーブルをCREATEする0003_testは実行されず、0004_test_column3から実施することになる。
その場合、「そんなテーブルはないよ」というエラーが再度発生することになる。
逆に言うと、0003_testまで遡って再度実行すれば事象が解決しそう、という方針がたつ。
これを実行するために、以下のコマンドを実行する。
python manage.py migrate tests 0002 --fake
このコマンドのポイントは2つある。
1つは 0002 のようにマイグレーションのバージョンを指定していることである。これは Reversing migrations と呼ばれ、指定したバージョン以降に実行したマイグレーションをなかったことにする処理になる。今回は0003のマイグレーションをなかったことにするために0002を指定している。また、すべてのマイグレーションに対してリバースする場合は、zero というオプションが指定可能である。
もう1つは、--fake というオプションを付けている点である。このオプションは簡単に言うと「マイグレーションは実行しないが、マイグレーションを実行したことにする」というオプションである。
このオプションを付けない場合、要するに以下のようなコマンドを実行する場合、
python manage.py migrate tests 0002
0003_test のマイグレーションをなかったことにするため、スキーマに対する処理としては「test テーブルをDROPする処理が走る」ことになる。ただ、test テーブルは既に手動で削除されて存在しないので、DROPする対象がない、というエラーが発生することになる。 これを避けてマイグレーションを遡るために、--fake オプションをつけることによって、「リバースマイグレーションをしたことにする」ことが可能になる。
上記のコマンド実行後、再度showmigrationsをすると、
(py39) C:\Users\XXX\TestProject>python manage.py showmigrations admin [X] 0001_initial [X] 0002_logentry_remove_auto_add [X] 0003_logentry_add_action_flag_choices auth [X] 0001_initial [X] 0002_alter_permission_name_max_length [X] 0003_alter_user_email_max_length [X] 0004_alter_user_username_opts [X] 0005_alter_user_last_login_null [X] 0006_require_contenttypes_0002 [X] 0007_alter_validators_add_error_messages [X] 0008_alter_user_username_max_length [X] 0009_alter_user_last_name_max_length [X] 0010_alter_group_name_max_length [X] 0011_update_proxy_permissions [X] 0012_alter_user_first_name_max_length contenttypes [X] 0001_initial [X] 0002_remove_content_type_name sessions [X] 0001_initial tests [X] 0001_initial [X] 0002_test1_column3 [ ] 0003_test [ ] 0004_test_column3
0003_testのXが外れている(=未反映であると認識されている)ことが分かる。
この状態で再度マイグレーションを実行すると、
(py39) C:\Users\XXX\TestProject>python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions, tests Running migrations: Applying tests.0003_test... OK Applying tests.0004_test_column3... OK
となり、無事エラーが解消したことが分かる。
まとめ
Djangoのマイグレーションで OperationalError: no such table エラーが発生したときの解決方法をまとめた。
そもそもの話だが、再現方法に書いたようなマイグレーション以外の方法で直接DBスキーマをいじるのは極力避け、Djangoのマイグレーション機能のみを使ってスキーマを管理していくのがベストプラクティスだと思う。