こいけるの日記

データサイエンス / データエンジニアリングに興味がある若手SIer社員の日記

【Django】マイグレーション時のエラー OperationalError: no such table を解決する

概要

Djangoを使ってちょっとしたアプリを作っていた時に、マイグレーション実行時以下のようなエラーが出てきた。

django.db.utils.OperationalError: no such table: tests_test1

エラーの原因とそれに対する解決方法をメモしておく。

バージョン情報

Django 3.1.4 を使用

事象の再現

以下のようなオペレーションを行うことによって、事象を再現することが可能。

  1. マイグレーションで新しいテーブルを作成
  2. 1で作成したテーブルを手動で消去
  3. マイグレーションでテーブルに対する変更を実行

詳細に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. 適用済みのマイグレーションを再度実行する
  2. マイグレーションファイルを手で編集し、実行する
  3. 手動で直接テーブルを作成する

ここでは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マイグレーション機能のみを使ってスキーマを管理していくのがベストプラクティスだと思う。