こんにちは!内藤です!
久しぶりの更新です。
DjangoのModelはクラスを継承させることができ、フレームワークでうまいことやってくれるのでとても便利です。
共通部分を親クラスに、個別部分を子クラスに定義することで、
・検索画面 → 親クラスを対象にする
・詳細画面 → 子クラスを対象にする
のような実装をすることができます。
先日、子クラスで定義していたフィールドを検索対象にするために、親クラスに移動する必要がありました。
しかも既存のテストデータが存在しているので、設定済みのデータの移行も必要でした。
ChatGPTと二人三脚で対応したのですが、なかなか面倒だったのでまとめておきます。
TL;DR
- 普通にフィールド移動してmakemigrationsするとデータ移行のチャンスがなくなる
- 下記の手順でmigrationをする
- 移行先に別名でフィールドを追加
- migrationファイル中でフィールドの値をコピー
- 移行元のフィールドを削除
- 移行先でフィールド名を変更
- 移行先に別名でフィールドを追加
やりたいこと
以下のようなモデルを例にします。
from django.db import models
class Parent(models.Model):
field_a = models.CharField(max_length=100)
class Child(Parent):
field_b = models.CharField(max_length=100)
field_c = models.CharField(max_length=100)
makemigrationsすると、下記のようなファイルが生成されます
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Parent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('field_a', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Child',
fields=[
('parent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='main.parent')),
('field_b', models.CharField(max_length=100)),
('field_c', models.CharField(max_length=100)),
],
bases=('main.parent',),
),
]
ここから、Parentクラスにfield_bフィールドを追加して、Childクラスのfield_bのデータを保ったままにしたい
実現手順
では、実際に作業していきましょう
Parentクラスに別名のフィールドを追加して、子クラスのデータを引き継ぐ
Parentクラスにtemp_field_bフィールドを追加します
from django.db import models
class Parent(models.Model):
field_a = models.CharField(max_length=100)
temp_field_b = models.CharField(max_length=100, default="")
class Child(Parent):
field_b = models.CharField(max_length=100)
field_c = models.CharField(max_length=100)
ここで makemigrations を実行すると、下記のようなマイグレーションファイルが生成されます
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='parent',
name='temp_field_b',
field=models.CharField(default='', max_length=100),
),
]
このファイルを編集して、追加したフィールドに子クラスから既存のデータを引き継ぐ処理を追記します
from django.db import migrations, models
def copy_field(apps, schema_editor):
"""親クラスに追加したフィールドに子クラスからデータをコピー"""
Parent = apps.get_model("main", "Parent")
Child = apps.get_model("main", "Child")
for p in Parent.objects.all():
c = Child.objects.get(id=p.id)
p.temp_field_b = c.field_b
p.save()
class Migration(migrations.Migration):
dependencies = [
('main', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='parent',
name='temp_field_b',
field=models.CharField(default='', max_length=100),
),
migrations.RunPython(copy_field),
]
子クラスのフィールドを削除
不要になった、子クラスのフィールドを削除します
from django.db import models
class Parent(models.Model):
field_a = models.CharField(max_length=100)
temp_field_b = models.CharField(max_length=100, default="")
class Child(Parent):
field_c = models.CharField(max_length=100)
ここで makemigrations を実行すると、下記のようなマイグレーションファイルが生成されます
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0002_parent_temp_field_b'),
]
operations = [
migrations.RemoveField(
model_name='child',
name='field_b',
),
]
親クラスのフィールド名を変更する
親クラスに追加したフィールドを本来の名前に変更します
from django.db import models
class Parent(models.Model):
field_a = models.CharField(max_length=100)
field_b = models.CharField(max_length=100, default="")
class Child(Parent):
field_c = models.CharField(max_length=100)
makemigrations を実行すると、フィールドをrenameしたか?と聞かれるので、yを選択
python manage.py makemigrations
Was parent.temp_field_b renamed to parent.field_b (a CharField)? [y/N] y
生成されたマイグレーションファイルは、下記になります
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0003_remove_child_field_b'),
]
operations = [
migrations.RenameField(
model_name='parent',
old_name='temp_field_b',
new_name='field_b',
),
]
これで、やりたいことが実現できました!
終わりに
SQLだと単純な作業も、ちょっと遠回りが必要でした。
makemigrations で生成したファイルを編集する機会はあまりありませんが、色々と応用がききそうです。