DBのトランザクションの概要について理解する

:::note
### 概要
トランザクションとは
複数の SQL 文によるデータ更新を1つの処理としてまとめてデータベースに反映させることです。
一部だけがデータベースに反映されると、データベース全体としてデータが不整合な状態になる場合に有効です。
:::

# 背景 🖼️

業務でトランザクションを使ったのですが、口で説明できるほど概要がつかめていないので今回復習します。

# トランザクションとは 💡

https://oss-db.jp/dojo/dojo_01

複数の SQL 文によるデータ更新を1つの処理としてまとめてデータベースに反映させることを言います。

一部だけがデータベースに反映されると、データベース全体としてデータが不整合な状態になる場合に有効です。

たいせい君がお父さんに送金する際の処理を考えます。（本当に送金してるかはそっとしておいてください）

:::note
Laravelで検証します。
:::

こちらで動作確認可能です。

https://github.com/yamatai12/transaction-app

以下に公式docを参考にします。

https://laravel.com/docs/11.x/database#database-transactions

# テーブル

accounts(口座テーブル)
口座の情報を保持します。

| Column        | Type          | Description                    |
|---------------|---------------|--------------------------------|
| id   | INT           | ID（主キー）             |
| name          | VARCHAR(50)   | ユーザ名                           |
| balance | INT           | 残高             |

---

tradingHistories(取引履歴テーブル)
取引情報を保持します。

| Column          | Type          | Description                    |
|-----------------|---------------|--------------------------------|
| id    | INT           | ID（主キー）               |
| sender_id  | INT   | accountsテーブルのID（外部キー）|
| receiver_id  | INT   | accountsテーブルのID（外部キー）|
| trading_amount  | INT   | accountsテーブルのID（外部キー）|

# サンプルデータ挿入

リアリティあるデータを挿入します😢

accounts(口座テーブル)
| id | name   | balance   |
|----|--------|-----------|
|  1 | Taisei |    100000 |
|  2 | Father | 100000000 |

# トランザクションなしでクエリを発行する

お父さんに1000円送金します。

```transaction-app/routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Models\Account;
use App\Models\TradingHistory;

Route::get('/', function () {

    $amount = 1000;
    $sender = Account::where('id', 1)->first();
    $receiver = Account::where('id', 2)->first();

    $sender->balance -= $amount;
    $receiver->balance += $amount;
    $sender->save();
    $receiver->save();

    TradingHistory::create([
        'sender_id' => $sender->id,
        'receiver_id' => $receiver->id,
        'trading_amount' => $amount,
    ]);

    return view('welcome');
});

```

クエリ発行後のテーブルは以下です。

accounts(口座テーブル)

| id | name   | balance   | updated_at          | created_at          |
|----|--------|-----------|---------------------|---------------------|
|  1 | Taisei |     95000 | 2024-11-04 10:18:54 | 2024-11-04 10:14:33 |
|  2 | Father | 100005000 | 2024-11-04 10:18:54 | 2024-11-04 10:14:33 |

tradingHistories(取引履歴テーブル)

| id | sender_id | receiver_id | trading_amount | updated_at          | created_at          |
|----|-----------|-------------|----------------|---------------------|---------------------|
|  1 |         1 |           2 |           1000 | 2024-11-04 10:18:54 | 2024-11-04 10:18:54 |

# トランザクションなしでクエリを発行する際にエラーを起こし、不整合な状態にする

```transaction-app/routes/web.php
$sender->save();
throw new \Exception();
$receiver->save();
```

お父さんに1000円送金しましたが、お父さんの口座に届いてないです。
これは不整合です。

accounts(口座テーブル)

| id | name   | balance   | updated_at          | created_at          |
|----|--------|-----------|---------------------|---------------------|
|  1 | Taisei |     94000 | 2024-11-04 10:25:37 | 2024-11-04 10:14:33 |
|  2 | Father | 100005000 | 2024-11-04 10:18:54 | 2024-11-04 10:14:33 |

tradingHistories(取引履歴テーブル)

| id | sender_id | receiver_id | trading_amount | updated_at          | created_at          |
|----|-----------|-------------|----------------|---------------------|---------------------|
|  1 |         1 |           2 |           1000 | 2024-11-04 10:18:54 | 2024-11-04 10:18:54 |

# トランザクションを使用する ✨

transaction内のクロージャが正常に実行されると、トランザクションは自動的にコミットされてDBに反映されます。
クロージャー内で例外がスローされた場合、トランザクションは自動的にロールバックされ、例外は再度スローされます。

```概念図
+---------------------+
|   トランザクション  |
|                     |
| +-----------------+ |
| |   SQLステートメント   | |
| | (データ操作、ここで複数のクエリが発行される)      | |
| +-----------------+ |
|         |           |
|         v           |
|    +----+----+      |
|    |         |      |
|    v         v      |
| +-------+   +------+ |
| | COMMIT |   | ROLLBACK | |
| +-------+   +------+ |
|     |            |     |
|     |            |     |
|     |            |     |
|     v            v     |
|  変更を保存      |   |  変更を取り消す    |
|   (データベース)  |   |  (データベース)    |
+---------------------+
```



```transaction-app/routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\DB; // DBファサードをインポート
use App\Models\Account;
use App\Models\TradingHistory;

Route::get('/', function () {
    $amount = 1000;

    // トランザクションを開始
    DB::transaction(function () use ($amount) {
        $sender = Account::where('id', 1)->first();
        $receiver = Account::where('id', 2)->first();

        // 残高の更新
        $sender->balance -= $amount;
        $receiver->balance += $amount;

        // 更新を保存
        $sender->save();
        $receiver->save();

        // 取引履歴の作成
        TradingHistory::create([
            'sender_id' => $sender->id,
            'receiver_id' => $receiver->id,
            'trading_amount' => $amount,
        ]);
    });

    return view('welcome');
});
```

次に送金してたいせい君の口座を保存してからエラーを起こします。

```transaction-app/routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\DB; // DBファサードをインポート
use App\Models\Account;
use App\Models\TradingHistory;

Route::get('/', function () {
    $amount = 1000;

    try {
        // トランザクションを開始
        DB::transaction(function () use ($amount) {
            $sender = Account::where('id', 1)->first();
            $receiver = Account::where('id', 2)->first();

            // 残高の更新
            $sender->balance -= $amount;
            $receiver->balance += $amount;

            // 更新を保存
            $sender->save();
            throw new \Exception();
            $receiver->save();

            // 取引履歴の作成
            TradingHistory::create([
                'sender_id' => $sender->id,
                'receiver_id' => $receiver->id,
                'trading_amount' => $amount,
            ]);
        });
    } catch (\Exception $e) {
        // トランザクションが失敗した場合の処理
    }

    return view('welcome');
});

```

テーブルを見ると、たいせい君の講座も更新されていないし、取引履歴のレコードも追加されていないです。
トランザクションによりDBの整合性が保たれています。

accounts(口座テーブル)

| id | name   | balance   | updated_at          | created_at          |
|----|--------|-----------|---------------------|---------------------|
|  1 | Taisei |     94000 | 2024-11-04 10:25:37 | 2024-11-04 10:14:33 |
|  2 | Father | 100005000 | 2024-11-04 10:18:54 | 2024-11-04 10:14:33 |

tradingHistories(取引履歴テーブル)

| id | sender_id | receiver_id | trading_amount | updated_at          | created_at          |
|----|-----------|-------------|----------------|---------------------|---------------------|
|  1 |         1 |           2 |           1000 | 2024-11-04 10:18:54 | 2024-11-04 10:18:54 |

# 参考 ✨

https://oss-db.jp/dojo/dojo_01

https://laravel.com/docs/11.x/database#database-transactions

# 最後に
- トランザクションに概要がつかめました
- ロックだったり、まだ理解できていない所を勉強します