M.Y. 雑感ブログ

札幌からWebの技術などを発信しています。

Laravel のDBトランザクション機能を見てみる

Laravel 5.4 からはトランザクションに関するメソッドが trait 化されているのでとても分かりやすい。

Concerns(関心) というくくりで trait 分けしているみたいです。その trait の使い方自体も面白いですね。

\Illuminate\Database\Concerns\ManagesTransactions(v5.4.11)

マルチプルトランザクション

まず、トランザクションは複数回かけることが出来そうです。

<?php
DB::beginTransaction(); // ここで `$this->getPdo()->beginTransaction();` が走る
DB::beginTransaction(); // ここでは `SAVEPOINT trans2;` が走る
DB::commit(); // ここは何もしない
DB::commit(); // ここで `$this->getPdo()->commit()` が走る

SAVEPOINT とは MySQL のリファレンス にある通り、トランザクション中の任意の場所にセーブポイントを作成し、ロールバック時にそのセーブポイント後の行操作だけを戻す、という動作を行えるようです。

セーブポイント後の行ロックは解放されないので注意ということも書いてありますね。

マルチプルトランザクションのロールバック

マルチプルトランザクションの途中でロールバックが発生した場合はどうなるでしょうか。

<?php
DB::beginTransaction();
DB::select('SELECT name FROM tbl_users WHERE id = 1;')[0]->name; // "aa"
DB::update("UPDATE users SET name = 'hoge' WHERE id = 1;");
DB::select('SELECT name FROM tbl_users WHERE id = 1;')[0]->name; // "hoge"

DB::beginTransaction();
DB::update("UPDATE users SET name = 'foo' WHERE id = 1;");
DB::select('SELECT name FROM tbl_users WHERE id = 1;')[0]->name; // "foo"

DB::rollback(); // `ROLLBACK TO trans2;` が走る
DB::select('SELECT name FROM tbl_users WHERE id = 1;')[0]->name; // "hoge"

DB::commit(); // `COMMIT;` する
DB::select('SELECT name FROM tbl_users WHERE id = 1;')[0]->name; // "hoge"

おお、途中まで戻って、コミットしたら途中までの結果がコミットされるんですね。

便利は便利なんですが、トランザクションの定義をしっかりしないとコワイことになりそう。

<?php
DB::transaction(function () {
  DB::update("UPDATE tbl_users SET name = 'hoge' WHERE id = 1;");
  DB::transaction(function () {
    DB::update("UPDATE tbl_users SET name = 'foo' WHERE id = 1;");
    throw new Exception;
  });
});

ちなみにこれも先ほどのコードと等価です。クロージャにより自動的にBEGINとCOMMIT(またはROLLBACK)を行ってくれるので、こちらの方が安全ですね。

デッドロック時の処理

デッドロックの場合にリトライ処理をさせることが出来ます。

<?php
$retries = 3;
DB::transaction(function () {
  DB::select('SELECT * FROM users WHERE id = 1 FOR UPDATE');
}, $retries);

これでトランザクション中にデッドロックしても、ロールバックして三回までリトライしてくれます。

<?php
$retries = 3;
DB::transaction(function () {
  DB::transaction(function () {
    DB::select('SELECT * FROM users WHERE id = 1 FOR UPDATE');
    DB::update("UPDATE users SET name = 'foo' WHERE id = 1;");
  });
}, $retries);

マルチプルトランザクションの場合にデッドロックが起こった時は、一旦全トランザクションを抜けなければならないので全てロールバックしてからリトライが行われます。