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 makemigrations
やpython manage.py migrate
が実行できなくなります。
この記事ではこのエラーの解決方法を解説します。
CircularDependencyError
CircularDependencyError
のCircular
は循環で、Dependency
は依存のことです。
直訳で「循環依存エラー」、つまり「循環参照エラー」ということになります。
先ほどのエラーをもう一度見てみましょう。
django.db.migrations.exceptions.CircularDependencyError: bob.0001_initial, alice.0001_initial
よく見るとbob
アプリとalice
アプリのマイグレーションファイル、0001_initial
を指さしているのがわかります。
bob.0001_initial
はbob/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'),
]
...
こっちはbob
の0001_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.py
のdependencies
から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.py
とalice/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.py
とalice/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を直接変更する前にワンクッション置くことができて楽ですね。
🦝 < アディオス、カウボーイ
🐭 < ユーの旅路に祝福を