DEV Community

k11o for Takenoko Tech LLC.

Posted on

Djangoで既存モデルにForeignKeyなフィールドを追加する場合のMigrationについて

Djangoでは割と使い物になるMigration生成機能がありますが、既存モデルにForeignKeyでフィールドを追加する場合、当該フィールドがNOT NULL制約付きなため、Migration時に適当な初期値を渡す必要があります。

具体的にコードで書くと、下記のような変更を行った場面です。(Postモデルのデータをいくつか作成したものの、TagでPostをまとめたくなった、という想定)

既存コード

class Post(models.Model):
    text = models.TextField("本文",blank=False)
Enter fullscreen mode Exit fullscreen mode

追加後

class Tag(models.Model):
    name = models.CharField("名前",max_length=10,blank=True)

class Post(models.Model):
    text = models.TextField("本文",blank=False)
    tag = models.ForeignKey(Tag,on_delete=CASCADE)
Enter fullscreen mode Exit fullscreen mode

この場合既存データが存在するため、普通にMigrationでフィールドを追加、というのが難しくなります。
そこでTagモデルのデータを作りつつ、既存のPostについては全て一旦生成したTagに紐づけるようなMigrationを、一回のMigration生成で行う方法を考えてみます。

先程のコードの変更を行ったあとに manage.py makemigrations を実行すると、既存データにNOT NULLな列が追加されるからデフォルト値が必要だよ!どうする?みたいなプロンプトが出るので、one-off default値を渡すことにして適当な値を突っ込みます(本来ならここでPythonコードが書けるのでTagモデルのデータを作りつつその値を渡す…ということが出来るのかも知れませんが、私が試した限りでは実現出来ませんでした。)

そうすると下記のようなMigrationコードが生成されます。

class Migration(migrations.Migration):

    dependencies = [
        ('application', '0011_auto'),
    ]

    operations = [
        migrations.AddField(
            model_name='post',
            name='tag',
            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='application.tag'),
            preserve_default=False,
        ),
    ]
Enter fullscreen mode Exit fullscreen mode

当然、このまま実行するとあまり良くない結果になるので、migrateコマンドは実行せずmigrationファイルを修正します。

処理の方針としては

  1. NOT NULL制約無しで列を追加
  2. Tagモデルのデータをとりあえず作り、既存データを紐づけるように更新
  3. NOT NULL制約を付ける

という順序でやってます。

migrationsにはRunPythonというMigration中に関数を実行する機能があるのでこれを利用します。因みにこの機能はRollback用関数も定義出来るので中々強力です。

またRunPythonで呼び出す関数については、ドキュメントにある通り引数の apps及びschema_editorを利用してDBへの接続やModelの取得を行います。

これらを踏まえ、前述した処理の通りに、既存のPostを全てTag「日常日記」に紐づけるようなMigrationに修正したものが下記です。

def create_tag(apps, schema_editor):
    db_alias = schema_editor.connection.alias
    Tag = apps.get_model("application", "Tag")
    tag = Tag.objects.using(db_alias).create(name="日常日記")
    Post = apps.get_model("application", "Post")
    Post.objects.using(db_alias).update(tag=tag)

class Migration(migrations.Migration):

    dependencies = [
        ('application', '0011_auto'),
    ]

    operations = [
        migrations.AddField(
            model_name='post',
            name='tag',
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='application.tag'),
            preserve_default=False,
        ),
        migrations.RunPython(create_tag),
        migrations.AlterField(
            model_name='post',
            name='tag',
            field=models.ForeignKey(null=False, on_delete=django.db.models.deletion.CASCADE, to='application.tag'),
            preserve_default=False,
        ),
    ]
Enter fullscreen mode Exit fullscreen mode

これで一つのmigrationで既存データを破壊せずに外部キーを追加する事が出来ました。
ただもっとスマートな方法がある気がしていますので、ご存知の方は是非コメント欄で教えていただけるとありがたいです。

Oldest comments (0)