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はスキーマ変更もトランザクション内で実行し、ロールバックできるので、何度実行しても同じになりますが、MySQLやOracleはスキーマ変更はトランザクションの対象外だからです。
MySQLの場合はスキーマ変更をして、データ更新に失敗した場合、スキーマ変更はロールバックされません。
この例だとカラムが残ったままま、マイグレーション失敗、という扱いになります。リカバリには手動でカラムを削除してやりなおす必要があります。
結論
スキーマ変更とデータ変更のマイグレーションは、同じクラスにまとめずに分けておいたほうが、失敗時のリカバリが楽です。