Help us understand the problem. What is going on with this article?

LaravelのDBトランザクション落とし穴

More than 3 years have passed since last update.

Laravelを使った開発でハマった事を何か紹介出来ればと思い書いてみました。

DBトランザクションはコネクション毎に効くようになっています

当たり前の事なんですけど、Laravelのような便利なフレームワークを利用していると
こういった常識を忘れて何時間もハマってしまったりします。

設定方法によっては同じDBにも関わらず、複数コネクション使用してしまっていて
期待通りにロールバックされないなんて事が起きるわけです。

もしかしたら一生役に立たない例かもしれませんが、
私が遭遇した落とし穴がどんなケースだったのかを紹介したいと思います。

DBのマスター・スレーブ構成を採用したアプリケーション

例えばマスター・スレーブ構成をとっている場合も、Laravelを使っていれば
設定ファイルに書き込み時のhostと読み込み時のhostを指定するだけで勝手に使い分けてくれます。とてもお手軽ですね。
https://readouble.com/laravel/5.3/ja/database.html#read-and-write-connections

config/database.phpで以下のように「read」と「write」を指定するだけでOKです。

'mysql' => [
    'read' => [
        'host' => '192.168.1.2', // スレーブのホストを指定
    ],
    'write' => [
        'host' => '196.168.1.1' // マスターのホストを指定
    ],
    'driver'    => 'mysql',
    'database'  => 'database',
    'username'  => 'root',
    'password'  => 'hogehoge',
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix'    => '',
],

言わずもがな裏ではマスターへのコネクションと、スレーブへのコネクションが作られるわけです。

読み込みだけどマスターを参照したい時

DBマスターへの書き込みがDBスレーブに同期されるまでにはタイムラグがあります。
通常気になる事はないでしょうけど、同期が失敗し続ける可能性もあります。

確実に最新情報を参照するためDBスレーブではなくマスターを参照したくなるわけです。

Laravelのモデルクラスではconnectionプロパティにて、利用する接続情報を指定する事ができます。
https://readouble.com/laravel/5.3/ja/eloquent.html#defining-models

この指定をすると デフォルトの接続情報よりも優先されるのです。
かゆいところに手が届く感じで良いですね!

config/database.php
// この設定を追記
'master' => [
    'host' => '192.168.1.1', // マスターと同じホストを指定
    'driver' => 'mysql',
    // 省略
],
店舗Model
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

/**
 * 店舗テーブル
 */
class Shop extends Model
{
    protected $table = 'shops';
}
商品Model
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

/**
 * 商品テーブル
 */
class Item extends Model
{
  protected $connection = 'master';
  // このようにモデルクラスでconnectionプロパティを指定する事で、
  // 商品テーブルへのアクセスは読み込みであってもマスターを向くようになります。

  protected $table = 'items';
}

これで意図したDBを参照するようになってくれます。
DBトランザクションの落とし穴も出来ました。

DBトランザクションを使用してみる

上記設定でDBトランザクションを使用してみましょう。

ShopとItemそれぞれへの更新がどちらもロールバックされるのが期待される動作です。

\DB::transaction(function() {

    $shop = new Shop;
    $shop->save();

    $item = new Item;
    $item->save();

    throw new \Exception('ここで処理を終わらせる');

});

しかし実際に動かしてみるとItemだけロールバックされません。
これは手動トランザクション(DB::beginTransaction()使うやつ)でも同じです。

なぜItemだけロールバックがされないのか?

実はItemモデルはconnectionにmasterを指定しているので
Shopと同じDBマスターを向いているにも関わらず、デフォルトとは別のコネクションを利用しちゃっているからなんです。

以下のように書くと理解しやすいかもしれません。
今度は逆で、Shopだけロールバックされなくなります。

\DB::connection('master')->transaction(function() {

    $shop = new Shop;
    $shop->save();

    $item = new Item;
    $item->save();

    throw new \Exception('ここで処理を終わらせる');

});

先ほどはトランザクションを開始する時にコネクションを明示していなかったので、
デフォルトのwriteコネクションにトランザクションが張られていたというわけです。

まとめ

原因がわかってしまえば何だそんな事かという感じですが、
ハマった当時はLaravelのバグだと決めつけて調べていました。猛省。。

参照するDBが同じだから同じコネクションを使用しているとは限りません。
そしてロールバックのような例外テストはしっかりやりましょうね。

stafes
お弁当&ケータリングの総合モール「ごちクル」を運営。”食を通じて世界をより幸せにする”ことに情熱を注ぐ会社です。
https://stafes.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away