🐯

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

に公開

はじめに

データベース更新処理を実装するとき、適切にトランザクション制御することが求められます。
この記事では 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 のトランザクション制御パターンです。
うまく使い分けていきたいです。

参考

Discussion