[Laravel] DB更新を行うときはトランザクションに気をつけよう

Laravel アドベントカレンダー2018  15日目です。

業務でLaravel書いていて、トランザクション管理されていないコード見たときに、
「あれ？ Laracvelってトランザクションのコード書かないときどういう動作してたっけ？」となったので、
Laravelのトランザクション処理について調べてみました。

# サンプルコード
https://github.com/yamatox/laravel-transaction-example

# 動作環境
Laravel 5.7
MySQL 5.7 （Auto Commitが有効）

# 前提条件

シナリオとしては、Aさんの口座からBさんの口座へ 2500円 送金する処理です。

```
口座番号：00001 残高：１００００円 持ち主：Aさん
口座番号：0000２ 残高：１００００円 持ち主：Bさん
```

# ケース一覧
- Case1 トランザクションを明示的に切らない かつ 送金が正常終了
- Case2 トランザクションを明示的に切らない かつ 送金中にエラーが発生
- Case3 トランザクションを明示的に切って かつ 送金中にエラーが発生
- Case4 トランザクションを明示的に切って かつ 送金が正常終了

## Case1 トランザクションを明示的に切らない かつ 送金が正常終了
[実行するコードはこちら](https://github.com/yamatox/laravel-transaction-example/blob/master/src/app/Console/Commands/Case1.php)

```console:PHP実行
/home/www/code # php artisan case1
/home/www/code # php artisan check #DBの内容を出力するだけのコマンドです
口座番号[000001]の残高は[7500]円です
口座番号[000002]の残高は[12500]円です
```

```console:SQLログ
Connect   root@172.23.0.3 on test_db using TCP/IP
Query     use `test_db`
Prepare   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
Execute   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
Close stmt
Prepare   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
Execute   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
Close stmt
Prepare   select * from `accounts` where `account_number` = ? limit 1
Execute   select * from `accounts` where `account_number` = '000001' limit 1
Close stmt
Prepare   update `accounts` set `balance` = ?, `updated_at` = ?where `id` = ?
Execute   update `accounts` set `balance` = 7500, `updated_at` = '2018-12-12 03:30:45' where `id` = 1
Close stmt
Prepare   select * from `accounts` where `account_number` = ? limit 1
Execute   select * from `accounts` where `account_number` = '000002' limit 1
Close stmt
Prepare   update `accounts` set `balance` = ?, `updated_at` = ?where `id` = ?
Execute   update `accounts` set `balance` = 12500, `updated_at`= '2018-12-12 03:30:45' where `id` = 2
Close stmt
Quit
```

何事もなく終わり、なにも問題のないコードに見えますね！
しかし。。。


## Case2 トランザクションを明示的に切らない かつ 送金中にエラーが発生
[実行するコードはこちら](https://github.com/yamatox/laravel-transaction-example/blob/master/src/app/Console/Commands/Case2.php)

```console:PHP実行
/home/www/code # php artisan case2 #Bの口座へ加算するタイミングでException発生
/home/www/code # php artisan check
口座番号[000001]の残高は[7500]円です
口座番号[000002]の残高は[10000]円です
# 2500円が闇に葬られました
```

## なぜ消えるのか？
Laravelでは、トランザクションの管理は行っていません。そのため、明示的にトランザクションを開始しない場合、MySQLのオートコミット設定により更新した時点で、内容が保存される（コミットされる）ためです。
[MySQL 参考ページ](https://dev.mysql.com/doc/refman/5.7/en/commit.html)

そのため、2500円減算する処理だけがDBに保存されてしまうのです。

```console:SQLログ
Connect   root@172.23.0.3 on test_db using TCP/IP
Query     use `test_db`
Prepare   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
Execute   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
Close stmt
Prepare   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
Execute   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
Close stmt
Prepare   select * from `accounts` where `account_number` = ? limit 1
Execute   select * from `accounts` where `account_number` = '000001' limit 1
Close stmt
# ここでトランザクションが始まって
Prepare   update `accounts` set `balance` = ?, `updated_at` = ?where `id` = ? 
Execute   update `accounts` set `balance` = 7500, `updated_at` = '2018-12-12 03:33:50' where `id` = 1 
Close stmt
#ここで終わっている（Commitされている）
Prepare   select * from `accounts` where `account_number` = ? limit 1
Execute   select * from `accounts` where `account_number` = '999999' limit 1
Close stmt
Quit
```

なので、以下のようにトランザクションを切ってあげます。

## Case3 トランザクションを明示的に切って かつ 送金中にエラーが発生
[実行するコードはこちら](https://github.com/yamatox/laravel-transaction-example/blob/master/src/app/Console/Commands/Case3.php)


```console:PHP実行
/home/www/code # php artisan case3 #Bの口座へ加算するタイミングでException発生
/home/www/code # php artisan check
口座番号[000001]の残高は[10000]円です
口座番号[000002]の残高は[10000]円です
```


```console:SQLログ
Connect   root@172.23.0.3 on test_db using TCP/IP
Query     use `test_db`
Prepare   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
Execute   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
Close stmt
Prepare   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
Execute   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
Close stmt
# ここでトランザクションが始まる
Query     START TRANSACTION 
Prepare   select * from `accounts` where `account_number` = ? limit 1
Execute   select * from `accounts` where `account_number` = '000001' limit 1
Close stmt
Prepare   update `accounts` set `balance` = ?, `updated_at` = ?where `id` = ?
Execute   update `accounts` set `balance` = 7500, `updated_at` = '2018-12-12 03:35:49' where `id` = 1
Close stmt
Prepare   select * from `accounts` where `account_number` = ? limit 1
Execute   select * from `accounts` where `account_number` = '999999' limit 1
Close stmt
Query     ROLLBACK
# ロールバック実行！
Quit
```
トランザクションを切っているので、Aさんの口座から減算するSQLを実行しても、まだコミットされていませんね。
そのため、Bさんの口座へ加算するタイミングでエラーになるとロールバックされてもとに戻ります。

ちなみに、トランザクションを切った状態できちんと処理が終了した場合は以下のようになります。

## Case4 トランザクションを明示的に切って かつ 送金が正常終了
[実行するコードはこちら](https://github.com/yamatox/laravel-transaction-example/blob/master/src/app/Console/Commands/Case4.php)

```console:PHP実行
/home/www/code # php artisan case4
/home/www/code # php artisan check
口座番号[000001]の残高は[7500]円です
口座番号[000002]の残高は[12500]円です
```

```console:SQLログ
Connect   root@172.23.0.3 on test_db using TCP/IP
Query     use `test_db`
Prepare   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
Execute   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
Close stmt
Prepare   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
Execute   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
Close stmt
Query     START TRANSACTION
Prepare   select * from `accounts` where `account_number` = ? limit 1
Execute   select * from `accounts` where `account_number` = '000001' limit 1
Close stmt
Prepare   update `accounts` set `balance` = ?, `updated_at` = ?where `id` = ?
Execute   update `accounts` set `balance` = 7500, `updated_at` = '2018-12-12 13:50:56' where `id` = 1
Close stmt
Prepare   select * from `accounts` where `account_number` = ? limit 1
Execute   select * from `accounts` where `account_number` = '000002' limit 1
Close stmt
Prepare   update `accounts` set `balance` = ?, `updated_at` = ?where `id` = ?
Execute   update `accounts` set `balance` = 12500, `updated_at`= '2018-12-12 13:50:56' where `id` = 2
Close stmt
Query     COMMIT
Quit
```

# まとめ
至極同然なお話ですが！
複数テーブルの更新を行う場合は、きちんとトランザクション管理をおこないましょう！
（自戒の念を込めて……）

#参考にさせていただいたページ

[(@suinさん)MｙSQLコンテナのクエリログを docker logs でみる方法](https://qiita.com/suin/items/95f15649a3ac39c8e9fb)

[Laravel 5.7 公式ドキュメント トランザクションについて](https://laravel.com/docs/5.7/database#database-transactions)

[MySQL5.7 公式ドキュメント コミットについて](https://dev.mysql.com/doc/refman/5.7/en/commit.html)