7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

LaravelAdvent Calendar 2018

Day 15

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

Last updated at Posted at 2018-12-15

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 トランザクションを明示的に切らない かつ 送金が正常終了

実行するコードはこちら

PHP実行
/home/www/code # php artisan case1
/home/www/code # php artisan check #DBの内容を出力するだけのコマンドです
口座番号[000001]の残高は[7500]円です
口座番号[000002]の残高は[12500]円です
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 トランザクションを明示的に切らない かつ 送金中にエラーが発生

実行するコードはこちら

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

なぜ消えるのか?

Laravelでは、トランザクションの管理は行っていません。そのため、明示的にトランザクションを開始しない場合、MySQLのオートコミット設定により更新した時点で、内容が保存される(コミットされる)ためです。
MySQL 参考ページ

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

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 トランザクションを明示的に切って かつ 送金中にエラーが発生

実行するコードはこちら

PHP実行
/home/www/code # php artisan case3 #Bの口座へ加算するタイミングでException発生
/home/www/code # php artisan check
口座番号[000001]の残高は[10000]円です
口座番号[000002]の残高は[10000]円です
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 トランザクションを明示的に切って かつ 送金が正常終了

実行するコードはこちら

PHP実行
/home/www/code # php artisan case4
/home/www/code # php artisan check
口座番号[000001]の残高は[7500]円です
口座番号[000002]の残高は[12500]円です
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さん)MySQLコンテナのクエリログを docker logs でみる方法

Laravel 5.7 公式ドキュメント トランザクションについて

MySQL5.7 公式ドキュメント コミットについて

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?