はじめに
Django のマイグレーションには atomic という属性があり、そのマイグレーションをトランザクションでどう囲むかを決めます。
atomic を理解すると、次のような挙動を意図して制御できるようになります。
- マイグレーションが途中で失敗したときに、どこまで残り、どこから消えるのか
- 大量データ移行を、1つの巨大なトランザクションにまとめるか、バッチに分割するか
- 失敗後に、安全に再実行できるか
この記事では、atomic の基本から、大きいデータ移行を安全に書く方法までをまとめています。
(バージョンは Django 5.2 LTS を基準にします)
atomic とは
原子性
atomic(アトミック)は「原子的」、つまりそれ以上分割できない一つのまとまりという意味です。
データベースでは、トランザクションが満たすべき性質 ACID の頭文字「A」= Atomicity(原子性) がこれにあたります。
原子性とは、トランザクション内の処理は「全部成功する」か「全部なかったことにする(ロールバック)」かのどちらかで、中途半端な状態を残さないという保証です。「all-or-nothing」とも言われます。
原子性についてACID特性の記事で書いています。
Django マイグレーションの atomic は、この原子性(全部成功か、全部ロールバックか)をマイグレーション全体に効かせるかどうかを決める設定です。
マイグレーションの atomic 属性
atomic は Migration クラスの属性で、デフォルトは True です。
class Migration(migrations.Migration):
atomic = False
operations = [...]
atomic = True は、そのマイグレーション内のすべての操作(テーブルの変更、RunPython によるデータの書き換え)を、まとめて1つのトランザクションで囲むことです。
つまり、マイグレーション全体が「全部成功するか、全部ロールバックするか」のまとまりになるということです。逆に atomic = False は、全体を1つにまとめません。
このように atomic は、マイグレーションのどこまでを1つのトランザクションにするかという境界を決めます。
True と False で何が変わるか
atomic = True(デフォルト)
atomic = True では、マイグレーション全体が1つのトランザクションになります。
途中で失敗すると、以下の図のようになります。
全体が1トランザクションなので、途中のどこかで失敗すれば、それまでの変更もすべてロールバックされます。中途半端な状態が残らないのが利点です。
一方で、大量データを更新するマイグレーションでは、デメリットもあります。
- 全体が完了するまで何もコミットされない(途中経過を保存できない)
- 失敗すると最初からやり直しになる
atomic = False
atomic = False では、マイグレーション全体を1つのトランザクションにしません。
途中で失敗すると、以下の図のようになります。
マイグレーションを1つのトランザクションで囲まないので、各操作は個別に確定されます。そのため、次のようなメリットがあります。
- 途中で失敗しても、それまでに更新できたデータはそのまま残る
- 処理済みのレコードをスキップする作り(冪等)にしておけば、再実行で続きから処理できる
大きいテーブルのデータ移行は、Django 公式ドキュメントでも atomic = False の使いどころとして挙げられています。
大きいデータ移行を安全に書く
atomic = False にすると、大きいデータ移行を「途中で失敗してもそこまで残り、再実行で続きから処理できる」形で書けます。
以下の例(公式の gen_uuid =各行に UUID を生成して埋めるデータ移行)では、「1000件ずつ」を1トランザクションにまとめています。
import uuid
from django.db import migrations, transaction
def gen_uuid(apps, schema_editor):
MyModel = apps.get_model("myapp", "MyModel")
while MyModel.objects.filter(uuid__isnull=True).exists():
with transaction.atomic(): # このブロックだけ1トランザクション
for row in MyModel.objects.filter(uuid__isnull=True)[:1000]:
row.uuid = uuid.uuid4()
row.save()
class Migration(migrations.Migration):
atomic = False
operations = [
migrations.RunPython(gen_uuid),
]
-
while ... .exists()と[:1000]:一度に全件ではなく、1000件ずつ、未処理がなくなるまで繰り返す -
filter(uuid__isnull=True):まだuuidが空の行だけを対象にするので、再実行すれば残りだけ処理される -
transaction.atomic():1000件を1回でコミットする。1件ずつより速く、各バッチは「全部成功 or 全部失敗」になる -
atomic = False:マイグレーション全体を1つのトランザクションにしない。途中で失敗しても、それまでの分は残る
atomic = False でないと、マイグレーション全体が1つのトランザクションになり、transaction.atomic() は独立してコミットされず、マイグレーション全体が終わるまで確定が保留されてしまいます。
補足:DB による前提
本記事は、トランザクショナル DDL に対応した PostgreSQL・SQLite を前提にしています。
MySQL・Oracle はスキーマ変更(DDL)をトランザクションで囲めません。そのため、図のような「マイグレーション全体を1つのトランザクションにまとめる」挙動にはなりません。
一方、データ更新(UPDATE など)のトランザクションには対応しているので、バッチごとに transaction.atomic() でコミットする書き方は MySQL でも動きます。
まとめ
atomic は、マイグレーションを1つのトランザクションで囲むかどうかを決める設定です。
- 全件を「すべて成功 or すべて失敗」にしたい → デフォルトの
atomic = True - 大きいデータ移行で途中保存・再実行したい →
atomic = False(バッチをtransaction.atomic()で囲み、冪等な処理にする)
参考

