Djangoのマイグレーションでスキーマ変更とデータ変更を分けたほうがよいか

Djangoマイグレーションは、1つのマイグレーションで複数のタスクを実行できます。

スキーマを変更する処理のあとに、データのマイグレーションを実行、といったようなものです。

意味のある変更をまとめるのは便利そうに思えるのですが、落とし穴があったりします。

例を挙げてみます。

コード

myapp/models.py (モデル)

from django.db import models


class Item(models.Model):
    col1 = models.CharField(max_length=30)
    col2 = models.CharField(max_length=30, blank=True, null=True)  # このカラムを追加するマイグレーション

myapp/migrations/0002_item_col2.py (マイグレーションコード)

from django.db import migrations, models


def migrate_data(apps, schema_editor):
    Item = apps.get_model('myapp', 'Item')
    db_alias = schema_editor.connection.alias
    Item.objects.all().update(col2=models.F('col1'))
    raise Exception("ここでエラー発生")


class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='item',
            name='col2',
            field=models.CharField(blank=True, max_length=30, null=True),
        ),
        migrations.RunPython(
            migrate_data,
            migrations.RunPython.noop
        )
    ]

実行例

このコードは意図的にマイグレーション中に例外を発生させていますが、実際にはマイグレーションコードが何らかの例外を発生させて落ちる状況を考えてください。

1回目の実行では、想定通りExceptionが発生して停止します。

Exception: ここでエラー発生

2回目の実行では、データベースにSQLite3を使っている場合は、1回目同様、Exceptionで落ちますが、MySQLを使っている場合はスキーマ変更のほうでエラーになります。

django.db.utils.OperationalError: (1060, "Duplicate column name 'col2'")

これは、SQLite3やPostgreSQLスキーマ変更もトランザクション内で実行し、ロールバックできるので、何度実行しても同じになりますが、MySQLOracleスキーマ変更はトランザクションの対象外だからです。

MySQLの場合はスキーマ変更をして、データ更新に失敗した場合、スキーマ変更はロールバックされません。

この例だとカラムが残ったままま、マイグレーション失敗、という扱いになります。リカバリには手動でカラムを削除してやりなおす必要があります。

結論

スキーマ変更とデータ変更のマイグレーションは、同じクラスにまとめずに分けておいたほうが、失敗時のリカバリが楽です。

参考