Djangoで循環参照しているマイグレーションを解決する

248, 2021-05-12

目次

Djangoで循環参照しているモデルをマイグレートする

Djangoで開発を進めていて、ある程度の規模になったプロジェクトを、いざ本番サーバーにデプロイしてみると意外にマイグレーション周りでエラーが頻発するものです。
その中のエラーの1つが↓のエラーです。

django.db.migrations.exceptions.CircularDependencyError: bob.0001_initial, alice.0001_initial

↑のエラーはマイグレーションファイルが循環参照しているために起こるエラーです。
このエラーが起こるとpython manage.py makemigrationspython manage.py migrateが実行できなくなります。
この記事ではこのエラーの解決方法を解説します。

CircularDependencyError

CircularDependencyErrorCircularは循環で、Dependencyは依存のことです。
直訳で「循環依存エラー」、つまり「循環参照エラー」ということになります。
先ほどのエラーをもう一度見てみましょう。

django.db.migrations.exceptions.CircularDependencyError: bob.0001_initial, alice.0001_initial

よく見るとbobアプリとaliceアプリのマイグレーションファイル、0001_initialを指さしているのがわかります。
bob.0001_initialbob/migrations/0001_initial.pyのことです。
これを見てみましょう。

class Migration(migrations.Migration):
    ...
    dependencies = [
        ('alice', '__first__'),
    ]
    ...

すると↑のように書かれている部分があります。
dependenciesとはこのマイグレーションファイルが依存しているマイグレーションのことです。
('alice', '__first__')と書かれていますが、aliceアプリの最初のマイグレーションファイル、つまり0001_initial.pyに依存しているということになります。

では続いてalice.0001_initial, つまりalice/migrations/0001_initial.pyを見てみます。

class Migration(migrations.Migration):
    ...
    dependencies = [
        ('bob', '0001_initial'),
    ]
    ...

こっちはbob0001_initial.pyに依存してます。
見事に循環参照ですね。
これはつまり、bobのマイグレーションを実行するには先にaliceのマイグレーションが必要だし、aliceのマイグレーションを実行するにはbobのマイグレーションが先に必要だということです。

このようなマイグレーションファイルがある状態でmigrateを行おうとすると先ほどのCircularDependencyErrorになります。

循環参照の解決

この循環参照をマイグレーションファイルの編集で解決する方法を今回は記述します。
まずなぜ先ほどのようなdependenciesの循環が発生するかと言うと、お互いのアプリがお互いのモデルを参照しているためです。
そのためマイグレーションファイルを作成すると、マイグレーションファイルの循環参照が起こります。
しかしモデル自体は別に循環で参照しているわけではないので、ここがややこしいところです。
つまりモデルの構造を変更しても、そのモデルレベルのレイヤーでは別になにも解決しないのです。
問題はマイグレーションのレイヤーで起こっているからです。

この問題を解決するにはまず、マイグレーションファイル内で別のアプリを参照してるモデルのフィールドに着目します。
具体的にはbob/migrations/0001_initial.pyを見てみましょう。

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('alice', '__first__'),
    ]

    operations = [
        migrations.CreateModel(
            name='Fire',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
            ],
        ),
        migrations.CreateModel(
            name='Leaf',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('water', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='alice.water')),
            ],
        ),
    ]

↑のマイグレーションの記述で、bobからaliceを参照しているモデルのフィールドは、Leafモデルのwaterフィールドです。
このフィールドはForeignKeyで定義されています。
to='alice.water'となっていますが、ここからaliceアプリのwaterモデルにリンクが張られているのがわかります。

つまり、このフィールドのマイグレーションを別のファイルに記述すれば、この0001_initial.pydependenciesからaliceへの依存を消せるということになります。

0002_fix.pyの作成

では先ほどのLeafモデルのwaterフィールドを0001_initial.pyから抽出し、0002_fix.pyというマイグレーションファイルに移植してみましょう。

なんだが外科手術みたいやね

気分は外科医やね

まず0001_initial.pyを↓のように編集します。

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = []

    operations = [
        migrations.CreateModel(
            name='Fire',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
            ],
        ),
        migrations.CreateModel(
            name='Leaf',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                #('water', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='alice.water')),
            ],
        ),
    ]

↑のコードで注目してほしいのはdependenciesが空になっているところと、Leafモデルのwaterフィールドがコメントアウトされているところです。
waterフィールドをコメントアウトしたので、このマイグレーションファイルはaliceのマイグレーションを必要としません。
言い方を変えると、このマイグレーションを実行するのにaliceアプリのモデルは必要ありません。
そのためdependenciesは空になります。依存先がないためです。

これでbob/migrations/0001_initial.pyはフリーになりました。aliceアプリのマイグレーションファイルとの相互参照は解決です。
しかしwaterフィールドをコメントアウトしたため、このままだとマイグレートでLeafモデルのwaterフィールドが作成されません。

ですのでbob/migrations/0002_fix.pyというマイグレーションファイルを手動で作成して対応します。
bob/migrations/0002_fix.pyは↓のような内容です。

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = False

    dependencies = [
        ('bob', '0001_initial'),
        ('alice', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='Leaf',
            name='water',
            field=models.ForeignKey(
                on_delete=django.db.models.deletion.PROTECT,
                to='alice.water',
            ),
        ),
    ]

0002_fix.pyは初期化ファイルではないのでinitial属性はFalseになります。
そしてこのマイグレーションファイルの実行にはbob/migrations/0001_initial.pyalice/migrations/0001_initial.pyをあらかじめ実行しておく必要があるので、dependenciesにそのような依存関係を書きます。

そしてoperationsにはAddFieldというオペレーションを追加します。
このオペレーションはモデルにフィールドを追加するオペレーションです。
ここではLeafモデルにwaterフィールドを追加するようにします。
このフィールドの定義は、先ほどのbob/migrations/0001_initial.pyから抽出したものと同じものです。

これでbob/migrations/0002_fix.pyが完成しました。

マイグレートの実行

循環参照が解決したので、あとはpython manage.py migrateを実行します。
するとbob/migrations/0001_initial.pyalice/migrations/0001_initial.pyが実行されたのちに、bob/migrations/0002_fix.pyが実行されます。

python manage.py showmigrationsを実行すると、実行したマイグレーションファイルの一覧が表示されます。

$ python manage.py showmigrations
...
alice
 [X] 0001_initial
bob
 [X] 0001_initial
 [X] 0002_fix
...

今回はbob側のマイグレーションファイルを編集して相互参照を解決しましたが、依存関係の解決はalice側からでも行えると思います。

おわりに

今回はDjangoのマイグレーションファイルの循環参照を解決してみました。
マイグレーションファイルは柔軟に編集ができるため、DBを直接変更する前にワンクッション置くことができて楽ですね。

アディオス、カウボーイ

ユーの旅路に祝福を