Djangoのmakemigrationsでエラーが発生した!

# はじめに

DjangoではDEFAULT_AUTO_FIELDを設定しないと次のような警告が表示される場合があります。

```text:python manage.py makemigrations実行時の警告メッセージ
System check identified some issues:                                                                                            
WARNINGS:                                                                                                                       
fobi.FormElement: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
        HINT: Configure the DEFAULT_AUTO_FIELD setting or the Config.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
...
```

ドキュメントを参照すると後から変更するのは大変だと書かれています。

https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

慎重に値を設定しようと参考書を確認すると、DEFAULT_AUTO_FIELDに言及した記述はないものの、個別にBigAutoFieldを利用している例が多いようです。

将来的にはdjangoのDEFAULT_AUTO_FIELDの初期値もAutoFieldからBigAutoFieldに変更されるだろうといった見通しをブログに書いている人もいるようです。

ということで、明示的にBigAutoFieldを指定したところ、これが悲劇の始まりとなりました。

```python:myapp/settings.py
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
```

:::note warn
もちろん使い方に依りますが、DEFAULT_AUTO_FIELDの値は明示的にデフォルトの``django.db.models.AutoField``を書いておいた方が良いでしょう。
:::

# 問題の背景

DjangoはObject-Relational Mapping(ORM)機構が備わっていて、サポートしているDBに依らず自動的にテーブルを作成してくれます。

個人的にはORMの機構が嫌いなのでRailsなども避けているのですが、Djangoを選択してしまったが故に利用することになりました。

その理由の一つにFobiというフォーム・デザイナーを利用しています。

https://github.com/barseghyanartur/django-fobi/

フォーム・デザイナーという性格上、カスタマイズしたフォームを登録して使うような場合もあるわけですが、こういったライブラリの機能を直接拡張するような方法はORMの機構と相性の良くない場合があります。

## Fobi側の未設定が原因

ワーニングメッセージはこのライブラリがapps.pyなどでConfigクラスのサブクラスを定義している中で、``default_auto_field``が未定義となっているため発生しました。

venv/myapp/lib/の中のapps.pyをみると、設定しているライブラリの方が少数のようです。

この点ではFobiが悪いということではなく、利用方法によっては他の場面でも遭遇する問題のように思えます。

結果的にWarningが発生しDEFAULT_AUTO_FIELDを設定することになったのですが、このライブラリの作成者は当然システムデフォルトのAutoFieldを想定しています。

ライブラリは個別にmigrationsファイルを持っているのですが、makemigrationsを実行した時にAutoFieldからBigAutoFieldに変更しようと利用しているライブラリにmigrationsファイルが追加されてしまいます。

```bash:
$ python3 manage.py makemigrations
```

この結果venv以下に新しくmigrationファイルが作成されてしまいます。

```python:makemigration実行時のメッセージ
Migrations for 'fobi':
  venv/myapp/lib/python3.12/site-packages/fobi/migrations/0016_alter_formelement_id_alter_formelemententry_id_and_more.py
    - Alter field id on formelement
    - Alter field id on formelemententry
    - Alter field id on formentry
    - Alter field id on formfieldsetentry
    - Alter field id on formhandler
    - Alter field id on formhandlerentry
    - Alter field id on formwizardentry
    - Alter field id on formwizardformentry
    - Alter field id on formwizardhandler
    - Alter field id on formwizardhandlerentry
Migrations for 'fobi_contrib_plugins_form_handlers_db_store':
  venv/myapp/lib/python3.12/site-packages/fobi/contrib/plugins/form_handlers/db_store/migrations/0003_alter_savedformdataentry_id_and_more.py
    - Alter field id on savedformdataentry
    - Alter field id on savedformwizarddataentry
```

変更の内容はいずれも``default_auto_field``についてのものでした。

```python:0016_alter_formelement_id_alter_formelemententry_id_and_more.pyの抜粋
    ...
    operations = [
        migrations.AlterField(
            model_name='formelement',
            name='id',
            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
        ),
        ...
```

コンテナ作成時のこの情報が引き継がれずに問題が発生していました。

:::note
こうなったのはカスタマイズしたフォームのモデルをORM機構がチェックした時に、アプリ側にはデフォルト値が指定されていたこと、ライブラリ側にはデフォルト値の指定がなかったことからテーブル間の整合性を取るために変更が必要と判断されたためでしょう。
:::

# 問題の詳細

ここまで分かれば対応は簡単なのですが、通常の利用時には分かりにくいエラーが発生します。

この状態で``python3 manage.py makemigrations``を実行すると次のようなエラーになる場合があります。

```bash:
$ python3 manage.py makemigrations
```

```python:
Traceback (most recent call last):
  File ".../manage.py", line 22, in <module>
    main()
.......
  File ".../venv/myapp/lib/python3.12/site-packages/django/db/migrations/graph.py", line 60, in raise_error
    raise NodeNotFoundError(self.error_message, self.key, origin=self.origin)
django.db.migrations.exceptions.NodeNotFoundError: Migration form.0001_initial dependencies reference nonexistent parent node ('fobi', '0016_alter_formelement_id_alter_formelemententry_id_and_more')

```

いきなりこのメッセージに遭遇すると、何が悪いのか、なかなか容易に判断できません。

ORM機構に起因するのは当然分かるので、migrationファイルを再生成しようとするかもしれませんが、後述するようにそういった方法では解決できません。

このままではmigrateも実行できずにエラーが発生してORM機構の正常性は損われたままとなってしまいます。

アプリケーションの軽微な修正は可能ですが、フィールドの追加などをしようものならちゃんと動かなくなってしまいます。

## いつエラーが発生するのか

``pip install -r requirements.txt``を実行する時には、venv環境を利用して開発機の影響はできるだけ排除しています。

一般的にvenvディレクトリはgitなどのVCSの管理下からは外して、必要なライブラリは``requirements.txt``などのファイルで管理して都度pipコマンドなどで導入します。

コンテナを作成する際には、アプリケーションの特定バージョンを``git clone``するなどして、Dockerfileの中で``python3 -m venv ...``や``pip3 install -r requimements.txt``などを実行します。

コンテナ環境のPythonは開発環境と同じバージョンでも、より新しいパッチレベルを利用したり、ARM64からAMD64用のバイナリ(あるいはその逆)を作成したりするので``*.pyc``ファイルと同様に依存関係の強いファイルはそのままコピーして利用できないので、コンテナ作成時に準備します。

このためアプリケーションのコードにはmigration関連のファイルが含まれていますが、ライブラリ側には存在しないことになり同期が取れなくなります。

Djangoを利用する上で、利用するライブラリ側にmigrations関連のファイルを変更してしまうのはとても大きな問題になることが分かりました。

# KubernetesとDjango

DjangoアプリケーションをKubernetesで利用する際の問題は、データベースの初期化です。

GKEのチュートリアルは、外部の端末から本番系のDBに接続するような内容なので、まともな本番環境を準備するような場面では役に立たないでしょう。

https://cloud.google.com/python/django/kubernetes-engine?hl=ja

いろんな方法を試しましたが、現在は開発環境で空のデータベースを作成してから、migrateやcreatesuperuserを実行して本番環境でリストアする方法を採用するようになりました。

:::note
データベースのリストア作業はアプリケーションが動作しなくてもPodが単独で起動していればexecでmysqlコマンドを呼び出すことで可能なので、この方法を好んで利用するようになりました。

本来はOperatorを使ってバックアップも定期的に取得するべきなのですが、Oracle謹製のOperatorは2.0.10の頃に試した範囲では起動して利用はできるものの、定期的に動作しなくなる問題があって利用を止めたままになっています。
:::

リストアとはいってもexportしたファイルはただのSQLの羅列なので、必要であれば特権ユーザーのパスワードなどは変更可能です。

もちろんPodの起動時に``python3 manage.py migrate``を実行する必要はあるのですが、初期構築時には``python3 manage.py createsuperuser``などの管理系のコマンドを実行する必要のないようにすることでコンテナイメージの作成を含めて考慮点は少なくなったと思います。

## コンテナと古いフレームワークの相性の悪さ

初期化作業をするために特殊な設定でPodを起動しなければいけなかったり、初期化用のシェルを実行するコンテナ準備したり、あまつさえ外部から本番環境のDBに接続して初期化作業を行うなどという蛮行などの例外的な作業はない方が良いわけです。

データベースの整合性が失なわれると直接テーブルを操作しないといけないのがORMを利用する際の一番の問題です。

データベースアドミン(DBA)がちゃんといて、開発者もORMが何をやっているか理解していれば良いのですが、私個人としてはあまり利用したくないというのが本音です。

もちろんデータベースの不整合の発生も想定して、アプリケーションレイヤーでデータをexportしてimportするようなロジックをちゃんと作れるのであれば問題ないです。

# さいごに

分かっている人がちゃんと使っているのか疑問ですが、ORM自体は便利な機能で、Djangoはそのパワーをうまく引き出しています。

ただコンテナは大量生産品なので乱暴に扱ってもちゃんと機能することが求められます。

昔は物理マシンにWebサーバーを入れて動かしていたので、開発機と本番機の線引きが曖昧でも問題なく、むしろそのような環境で扱いやすいようにフレームワークが進化したと思います。

Djangoに限らず2000年代のWebフレームワークを含むコンテナは羃等性を持たずオートクチュールのような繊細な扱いが必要になる場合が多い印象です。

全般的によく作り込まれている一方で環境変化に弱いという印象です。

あの当時、context-rootの変更といった可搬性、テスト・ステージ・本番環境といった環境変化を意識していたフレームワークはJ2EE以外にはなかったと思います。

またシンプル化した他の例としては、XMLが挙げられるでしょう。

機能が豊富で完璧すぎたが故に重く、直感的には使いにくく、JSON/YAML/TOMLといった流れが出来てしまいました。

結果的に標準化されていない方法でJSONのSchema/Validatorが普及し、我々は後からパッチを当てるような作業をする世界に生きています。

同様に将来的にはDjangoも、より単純化されたものが好まれるようになると思いますが、フレームワークとしては本当に良く出来ているので用途によっては使っていきたいと思います。