Laravel アドベントカレンダー2018 15日目です。
業務でLaravel書いていて、トランザクション管理されていないコード見たときに、
「あれ? Laracvelってトランザクションのコード書かないときどういう動作してたっけ?」となったので、
Laravelのトランザクション処理について調べてみました。
サンプルコード
動作環境
Laravel 5.7
MySQL 5.7 (Auto Commitが有効)
前提条件
シナリオとしては、Aさんの口座からBさんの口座へ 2500円 送金する処理です。
口座番号:00001 残高:10000円 持ち主:Aさん
口座番号:00002 残高:10000円 持ち主:Bさん
ケース一覧
- Case1 トランザクションを明示的に切らない かつ 送金が正常終了
- Case2 トランザクションを明示的に切らない かつ 送金中にエラーが発生
- Case3 トランザクションを明示的に切って かつ 送金中にエラーが発生
- Case4 トランザクションを明示的に切って かつ 送金が正常終了
Case1 トランザクションを明示的に切らない かつ 送金が正常終了
/home/www/code # php artisan case1
/home/www/code # php artisan check #DBの内容を出力するだけのコマンドです
口座番号[000001]の残高は[7500]円です
口座番号[000002]の残高は[12500]円です
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 トランザクションを明示的に切らない かつ 送金中にエラーが発生
/home/www/code # php artisan case2 #Bの口座へ加算するタイミングでException発生
/home/www/code # php artisan check
口座番号[000001]の残高は[7500]円です
口座番号[000002]の残高は[10000]円です
# 2500円が闇に葬られました
なぜ消えるのか?
Laravelでは、トランザクションの管理は行っていません。そのため、明示的にトランザクションを開始しない場合、MySQLのオートコミット設定により更新した時点で、内容が保存される(コミットされる)ためです。
MySQL 参考ページ
そのため、2500円減算する処理だけがDBに保存されてしまうのです。
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 トランザクションを明示的に切って かつ 送金中にエラーが発生
/home/www/code # php artisan case3 #Bの口座へ加算するタイミングでException発生
/home/www/code # php artisan check
口座番号[000001]の残高は[10000]円です
口座番号[000002]の残高は[10000]円です
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 トランザクションを明示的に切って かつ 送金が正常終了
/home/www/code # php artisan case4
/home/www/code # php artisan check
口座番号[000001]の残高は[7500]円です
口座番号[000002]の残高は[12500]円です
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さん)MySQLコンテナのクエリログを docker logs でみる方法