はじめに
こんにちは、Web エンジニアの鎌田です。
普段の業務では、フロントエンドに Angular(TypeScript)、バックエンドに Django(Python) を使っています。
今回はバックエンド開発のトピックです。
データベース更新処理を実装するとき、適切にトランザクション制御することが求められます。
この記事では Django のトランザクション制御についてまとめてみます。
注意
この記事では、Django REST framework を使った Web API としての実装を想定しています。
Django 単体で Web アプリケーションを開発する場合とは異なる部分があるかもしれません。
トランザクションとは
トランザクションとは、データベース更新処理の単位 のことです。
複数の一連の処理をひとまとまりとして扱いたいとき、それがひとつのトランザクションになります。
具体例
わかりづらいので具体例で考えてみましょう。
A さんの銀行口座(残高 50,000 円)から B さんの銀行口座(残高 20,000 円)に 10,000 円送金するとき、以下の 2 つの処理が発生します。
- A さんの口座から 10,000 円を引き出し、口座残高を 50,000-10,000=40,000 円にする
- B さんの口座に 10,000 円を振り込み、口座残高を 20,000+10,000=30,000 円にする
これらを別々に扱ったとします。
1 が成功し、2 が失敗したとすると、A さんの口座から 10,000 円減り、その 10,000 円がどこか消えてしまうことになります。
これが非常にまずいのは言うまでもありません。
このような不整合が発生しないように、1 と 2 の処理はひとまとまり、ひとつのトランザクションとして扱うべきです。
また、どちらかが失敗したら、どちらも処理前の状態に戻して、整合性を保つ必要があります。
データベースを処理前の状態に戻すことを ロールバック といいます。
ACID 特性
ちなみに、トランザクションは以下の ACID 特性 を満たす必要があると言われています。
ここでは、詳しい解説は割愛しますが、概要だけ示します。
-
原子性(Atomicity)
一連の操作がすべて実行されるか、ひとつも実行されないかのどちらかになること -
一貫性(Consistency)
データの状態に矛盾がないこと -
独立性(Isolation)
処理途中の結果が他に影響を与えないこと -
永続性(Durability)
処理が完了したら、その結果をずっと保持すること
Django のトランザクション制御パターン 3 選
1. HTTP リクエストごとにトランザクションを制御する
settings.py
で ATOMIC_REQUESTS
を True
に設定すると、ひとつの HTTP リクエストがひとつのトランザクションになります。
処理の途中で例外が発生したら、自動でロールバックしてくれます。
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'djangodb',
'USER': 'postgres',
'PASSWORD': 'postgres',
'HOST': 'localhost',
'PORT': '5432',
'ATOMIC_REQUESTS': True, # デフォルトはFalse
}
}
以下のように、@transaction.atomic
デコレーターを使って、ビュー全体をひとつのトランザクションにするのと同等です。
@transaction.atomic
def view(request):
# 処理
また、@transaction.non_atomic_requests
デコレーターを使うと、ATOMIC_REQUESTS
を無効にできます。
@transaction.non_atomic_requests
def view(request):
# 処理
2. 明示的にトランザクションを制御する
以下のように、コンテキストマネージャーを使うことでも、明示的にトランザクションを制御することができます。
def update_customer(customer_id: int) -> None:
try:
with transaction.atomic():
customer = Customer.objects.get(pk=customer_id)
# 処理
customer.save()
except IntegrityError:
# エラーハンドリング
celery
などを使って非同期 API を実装していると、ひとまとまりで扱いたい処理が複数の HTTP 通信にまたがります。
そのような場合、ATOMIC_REQUESTS
を有効にしていたとしても、明示的にトランザクションを制御する必要があります。
3. 低レイヤ API を使ってトランザクションを制御する
複数のテーブルを操作したり、ネストしたトランザクションを張る必要がある場合、手動でトランザクションを制御したいことがあります。
Django は、デフォルトでは自動コミットですが、以下のように、AUTOCOMMIT
を無効にすることで、手動でのトランザクション制御ができるようになります。
def process_order_and_payments(order_data, payments_data):
# AUTOCOMMITを無効にする
transaction.set_autocommit(False)
try:
# トランザクションの開始
sid = transaction.savepoint()
# 注文の作成
order = Order.objects.create(**order_data)
for payment_data in payments_data:
try:
# ネストされたトランザクションの開始
nested_sid = transaction.savepoint()
Payment.objects.create(order=order, **payment_data)
# ネストされたトランザクションのコミット
transaction.savepoint_commit(nested_sid)
except DatabaseError:
# 支払いの処理に失敗した場合、ネストされたトランザクションをロールバック
transaction.savepoint_rollback(nested_sid)
print(f"支払いの処理に失敗しました: {payment_data}")
# メイントランザクションのコミット
transaction.savepoint_commit(sid)
# 変更をデータベースに確定
transaction.commit()
except DatabaseError:
# メイントランザクションのロールバック
transaction.savepoint_rollback(sid)
transaction.rollback()
print("注文の処理に失敗しました")
finally:
# AUTOCOMMITを再有効化する
transaction.set_autocommit(True)
それぞれ、以下の SQL 文に対応するようです。
Django ORM | SQL |
---|---|
sid = transaction.savepoint() |
SAVEPOINT sid |
transaction.savepoint_commit(sid) |
RELEASE SAVEPOINT sid |
transaction.savepoint_rollback(sid) |
ROLLBACK TO SAVEPOINT sid |
おわりに
以上がよく使うであろう Django のトランザクション制御パターンです。
今回まとめてみて頭の中が整理された感覚があります。
今後、場面に応じた適切な実装を選択しやすくなる予感がしています。
不足等ありましたら、ご指導いただけると幸いです。
参考
-
Django 公式ドキュメント
https://docs.djangoproject.com/ja/5.0/topics/db/transactions/