【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.py` で `ATOMIC_REQUESTS` を `True` に設定すると、ひとつの HTTP リクエストがひとつのトランザクションになります。
処理の途中で例外が発生したら、自動でロールバックしてくれます。

```python
DATABASES = {
  'default': {
    'ENGINE': 'django.db.backends.postgresql_psycopg2',
    'NAME': 'djangodb',
    'USER': 'postgres',
    'PASSWORD': 'postgres',
    'HOST': 'localhost',
    'PORT': '5432',
    'ATOMIC_REQUESTS': True,  # デフォルトはFalse
  }
}
```

以下のように、`@transaction.atomic` デコレーターを使って、ビュー全体をひとつのトランザクションにするのと同等です。

```python
@transaction.atomic
def view(request):
    # 処理
```

また、`@transaction.non_atomic_requests` デコレーターを使うと、`ATOMIC_REQUESTS` を無効にできます。

```python
@transaction.non_atomic_requests
def view(request):
    # 処理
```

### 2. 明示的にトランザクションを制御する

以下のように、コンテキストマネージャーを使うことでも、明示的にトランザクションを制御することができます。

```python
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` を無効にすることで、手動でのトランザクション制御ができるようになります。

```python
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 のトランザクション制御パターンです。
うまく使い分けていきたいです。

## 参考

- キタミ式イラスト IT 塾 基本情報技術者 令和 04 年
  https://www.amazon.co.jp/%E3%82%AD%E3%82%BF%E3%83%9F%E5%BC%8F%E3%82%A4%E3%83%A9%E3%82%B9%E3%83%88IT%E5%A1%BE-%E5%9F%BA%E6%9C%AC%E6%83%85%E5%A0%B1%E6%8A%80%E8%A1%93%E8%80%85-%E4%BB%A4%E5%92%8C04%E5%B9%B4-%E3%81%8D%E3%81%9F%E3%81%BF-%E3%82%8A%E3%82%85%E3%81%86%E3%81%98/dp/4297124513

- Django 公式ドキュメント
  https://docs.djangoproject.com/ja/5.0/topics/db/transactions/