ユーニックス総合研究所

  • home
  • archives
  • django-fix-circular-migrations

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

  • 作成日: 2021-05-12
  • 更新日: 2023-12-24
  • カテゴリ: Django

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を直接変更する前にワンクッション置くことができて楽ですね。

🦝 < アディオス、カウボーイ

🐭 < ユーの旅路に祝福を