Djangoのマイグレーションシステムには、データベースのテーブル構造を変更する「スキーママイグレーション」の他に、migrations.RunPythonを使用してデータを操作する「データマイグレーション」機能があります。
データマイグレーションは、モデルのリファクタリング(例えば、古いカラムから新しいテーブルへデータを移し替える)の際に不可欠です。しかし、順方向(migrate)のロジックだけを実装し、逆方向(unmigrate、ロールバック)の処理を省略してしまうケースが少なくありません。
マイグレーションに問題があった場合、安全に元の状態に戻せることは、システムの信頼性にとって非常に重要です。この記事では、RunPythonのreverse_code引数を適切に実装し、データマイグレーションの可逆性を担保する方法について解説します。
データマイグレーションと可逆性の課題
例として、当初「記事(Article)」モデルに直接「公開日(published_on)」を持たせていた状態を考えます。
# 移行前の models.py (抜粋)
class Article(models.Model):
title = models.CharField("タイトル", max_length=255)
content = models.TextField("本文")
# リファクタリング対象のフィールド
published_on = models.DateField("公開日", null=True, blank=True)
# ...
このpublished_onフィールドを削除し、代わりに「公開ステータス(PublishingStatus)」という別モデル(記事へのOneToOneFieldとpublication_dateを持つ)に移行するリファクタリングを行うとします。
この時、migrations.RunPythonを使って、既存のArticle.published_onのデータを新しいPublishingStatusテーブルにコピーする順方向のデータ移行(code引数)を実装します。
しかし、もしreverse_code引数を指定しなかったり、migrations.RunPython.noop(何もしない)を指定したりした場合、このマイグレーションをロールバック(unmigrateコマンド)しようとすると、DjangoはIrreversibleError(不可逆エラー)を送出します。データが失われ、古い状態に戻せなくなってしまうためです。
順方向と逆方向のロジックを定義する
安全なデータマイグレーションを実装するには、順方向の操作(code)と、それを完全に取り消す逆方向の操作(reverse_code)の両方を関数として定義します。
これらは通常、manage.py makemigrations <app_name> --emptyで作成した空のマイグレーションファイル内に記述します。
順方向のデータ移行(Forward)
順方向の関数(例:migrate_publication_date)は、古いArticle.published_onの値を読み取り、新しいPublishingStatusモデルのインスタンスを作成します。
# 順方向: Article.published_on -> PublishingStatus.publication_date
def migrate_publication_date(apps, schema_editor):
Article = apps.get_model("blog", "Article")
PublishingStatus = apps.get_model("blog", "PublishingStatus")
# nullでない公開日を持つ記事を対象
articles_to_migrate = Article.objects.filter(published_on__isnull=False)
new_statuses = []
for article in articles_to_migrate:
new_statuses.append(
PublishingStatus(
article=article,
publication_date=article.published_on
)
)
if new_statuses:
PublishingStatus.objects.bulk_create(new_statuses)
逆方向のデータ移行(Reverse)
逆方向の関数(例:reverse_migration)は、順方向の操作と全く逆のことを行います。PublishingStatusのデータを読み取り、それをArticle.published_onフィールドに書き戻します。
# 逆方向: PublishingStatus.publication_date -> Article.published_on
def reverse_migration(apps, schema_editor):
Article = apps.get_model("blog", "Article")
PublishingStatus = apps.get_model("blog", "PublishingStatus")
# 移行対象のステータスを取得
statuses = PublishingStatus.objects.select_related("article").all()
articles_to_update = []
for status in statuses:
# Articleインスタンスに日付を書き戻す
article = status.article
article.published_on = status.publication_date
articles_to_update.append(article)
if articles_to_update:
# bulk_updateで効率的に更新
Article.objects.bulk_update(articles_to_update, ['published_on'])
migrations.RunPython への適用
これら2つの関数を、マイグレーションファイルのoperationsリスト内でmigrations.RunPythonに渡します。
code引数に、順方向の関数(migrate_publication_date)reverse_code引数に、逆方向の関数(reverse_migration)
# データ移行マイグレーションファイル (例: 0003_migrate_pubdate.py)
from django.db import migrations
# (ここに関数 migrate_publication_date と reverse_migration を定義)
# ...
class Migration(migrations.Migration):
dependencies = [
# このマイグレーションは、
# 1. PublishingStatusモデルが作成された後 (0002_create_publishingstatus)
# 2. Article.published_on が削除される前 (0004_remove_article_published_on)
# に実行される必要がある
('blog', '0002_create_publishingstatus'),
]
operations = [
migrations.RunPython(
code=migrate_publication_date,
reverse_code=reverse_migration
),
]
ロールバック処理の注意点
逆方向の移行を設計する際は、「順方向の移行によって失われる情報がないか」を考慮する必要があります。
今回の例ではOneToOneField(PublishingStatus)への移行だったため、データは1対1で対応が取れました。しかし、もし1対多の関係(例:価格履歴)に移行した場合、ロールバック時に「どの履歴を元の単一フィールドに戻すか」という設計上の判断が必要になります。
まとめ
データマイグレーションは、データベースの構造だけでなく、その中身のデータを直接変更する、影響の大きな操作です。
migrations.RunPythonを使用する際は、順方向のロジックだけでなく、reverse_code引数を用いて逆方向の(ロールバック)ロジックも必ず実装することが推奨されます。
これにより、マイグレーションの可逆性が確保され、万が一デプロイやテストで問題が発生した際にも、unmigrateコマンドによって安全に以前の状態へ復元することが可能となり、システムの堅牢性が向上します。
