4
3

【Django】トランザクション制御パターン 3 選

Last updated at Posted at 2024-08-06

はじめに

こんにちは、Web エンジニアの鎌田です。
普段の業務では、フロントエンドに Angular(TypeScript)、バックエンドに Django(Python) を使っています。

今回はバックエンド開発のトピックです。
データベース更新処理を実装するとき、適切にトランザクション制御することが求められます。
この記事では Django のトランザクション制御についてまとめてみます。

注意

この記事では、Django REST framework を使った Web API としての実装を想定しています。
Django 単体で Web アプリケーションを開発する場合とは異なる部分があるかもしれません。

トランザクションとは

トランザクションとは、データベース更新処理の単位 のことです。
複数の一連の処理をひとまとまりとして扱いたいとき、それがひとつのトランザクションになります。

具体例

わかりづらいので具体例で考えてみましょう。

A さんの銀行口座(残高 50,000 円)から B さんの銀行口座(残高 20,000 円)に 10,000 円送金するとき、以下の 2 つの処理が発生します。

  1. A さんの口座から 10,000 円を引き出し、口座残高を 50,000-10,000=40,000 円にする
  2. B さんの口座に 10,000 円を振り込み、口座残高を 20,000+10,000=30,000 円にする

これらを別々に扱ったとします。

1 が成功し、2 が失敗したとすると、A さんの口座から 10,000 円減り、その 10,000 円がどこか消えてしまうことになります。

これが非常にまずいのは言うまでもありません。
このような不整合が発生しないように、1 と 2 の処理はひとまとまり、ひとつのトランザクションとして扱うべきです。

また、どちらかが失敗したら、どちらも処理前の状態に戻して、整合性を保つ必要があります。
データベースを処理前の状態に戻すことを ロールバック といいます。

ACID 特性

ちなみに、トランザクションは以下の ACID 特性 を満たす必要があると言われています。
ここでは、詳しい解説は割愛しますが、概要だけ示します。

  1. 原子性(Atomicity)
    一連の操作がすべて実行されるか、ひとつも実行されないかのどちらかになること

  2. 一貫性(Consistency)
    データの状態に矛盾がないこと

  3. 独立性(Isolation)
    処理途中の結果が他に影響を与えないこと

  4. 永続性(Durability)
    処理が完了したら、その結果をずっと保持すること

Django のトランザクション制御パターン 3 選

1. HTTP リクエストごとにトランザクションを制御する

settings.pyATOMIC_REQUESTSTrue に設定すると、ひとつの 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 のトランザクション制御パターンです。
今回まとめてみて頭の中が整理された感覚があります。
今後、場面に応じた適切な実装を選択しやすくなる予感がしています。
不足等ありましたら、ご指導いただけると幸いです。

参考

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3