内藤 裕二/ 2025年 6月 5日/ 技術

こんにちは!内藤です!
久しぶりの更新です。

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 で生成したファイルを編集する機会はあまりありませんが、色々と応用がききそうです。

参照URL