【PHP】CakePHP4.X系で複数のモデルを同一トランザクションで処理したかった。

こんにちは。
もう少しわかりやすくしてほしい。
只野です。

はい。最近なんとなくCakePHPに触れている私です。しっかりと学習すれば強力なフレームワークなんでしょうが、私が思った感想は、DB系の処理がすげー触りにくい。です。

多分SQLとか直接DBへ命令する事を意識させないようにしているのでしょうが、SQLやDBの仕組みをある程度理解している身からするとここまでフレームワークで制御しちゃうの?とか思いました。

なぜこのような事を思ったかと言いますと、複数のテーブルへ登録または更新処理をしようとした時に私は1トランザクションでDB操作を実行し、何かしらエラーが発生したら全てロールバック(なかったことに)する処理を組もうとしたのですが、CakePHPのフレームワークで利用できる「Model」層と言われるDB操作系の処理が普通に使うとトランザクションを「Model」単位で発行していたのです。

「Model」層を完全に無視してDB処理を実装することもできましたが、「Model」層を完全に無視するとCakePHPを使う理由が薄れていくなと思い使う方向で考えました。

そんでまずこんな処理を書いてみますがこれは失敗例です。

use Cake\Datasource\ConnectionManager;
use Cake\Core\Exception\Exception;

public function sample() {
    //モデルロード
    $this->loadModel('table1');
    $this->loadModel('table2');
    //DB接続コネクション取得
    $connection = ConnectionManager::get('default');
    //トランザクション開始
    $connection->begin();

    try {
        $table1 = $this->table1->newEmptyEntity();
        //値設定とか
        $this->table1->save($table1);

        $table2 = $this->table2->newEmptyEntity();
        //値設定とか
        $this->table2->save($table2 );
        
        //コミット
        $connection->commit();
    } catch ( Exception $ex ) {
        //例外処理(ロールバック)
        $connection->rollback();
    }
}

上記のような形で処理を組み「table1」でDB処理成功後、「table2」の処理で何かした例外エラーが発生したとします。ここでは当然「table1」も例外処理に含まれているロールバックにより「table1」のDB操作内容が取消されていなければなりません。
ですが上記コードではロールバックされません。Web検索するとCakePHPの4.X以前のバージョンであれば上記内容でロールバックされます。との記事を多数見かけましたが、4.Xから仕様が変わったのか普通にコミットされています。

上記コード何が問題になるのか詳しく調べたところ、まずCakePHPのsaveメソッドはDB操作処理が正常終了の場合、デフォルトで「commit」するという事。余計なお世話です。

次にDBコネクションが何もしなければ「Model」単位で発行するという事。DBコネクションがどこかで一つだけ使うようにフレームワークで制御してくれと思います。「Model」が別であれば別々のコネクションが利用されています。上記例の場合、「table1」、「table2」は別々のコネクションが発行されています。

上記2点の問題を解消すれば1トランザクションでDB処理を全て解決できます。フレームワークのソースコードを直接見た結果、複数のモデルを利用する場合に同一トランザクションで処理するには、下記実装が一応解決策になります。

use Cake\Datasource\ConnectionManager;
use Cake\Core\Exception\Exception;

public function sample() {
    //モデルロード
    $this->loadModel('table1');
    $this->loadModel('table2');
    //DB接続コネクション取得
    $connection = ConnectionManager::get('default');
    //トランザクション開始
    $connection->begin();
    //Modelへ利用するコネクションを設定
    $this->table1->setConnection($connection);
    $this->table2->setConnection($connection);

    try {
        $table1 = $this->table1->newEmptyEntity();
        //値設定とか
        //オートコミット解除
        $this->table1->save($table1, ['atomic' => false]);

        $table2 = $this->table2->newEmptyEntity();
        //値設定とか
        //オートコミット解除
        $this->table2->save($table2 ['atomic' => false]);
        
        //コミット
        $connection->commit();
    } catch ( Exception $ex ) {
        //例外処理(ロールバック)
        $connection->rollback();
    }
}

追加した処理はモデルへ利用するコネクションの設定とオートコミットの解除です。この処理を含めなければ同一トランザクションで別々のモデルの処理結果を同一タイミングでコミットまたはロールバックすることができませんでした。

CakePHPが複数のテーブルへデータを保存する場合は事前にモデルの設定でアソシエーションと呼ばれる関連付けをしてくださいという方針なので、直接関係ないテーブルへの保存タイミングまで管理するようになっていないのでしょう。

基本的な処理の流れはわかっていても、フレームワークでの命令の仕方がわからずにかなり戸惑いました。私的にはフレームワークはユーザーの補助程度だとありがたいと考えます。CakePHPは規約を守れと利用者に押しているので、私には合わないフレームワークなのだろうとも感じます。

本日はこの辺で

2件のコメント

  1. こんにちは。記事参考になりました。
    save処理は失敗しても例外を投げないので、
    最初の方法でもsave結果がfalse時に例外をスローするようにすれば戻りますよ。
    横から失礼しました。

    1. こんにちは。技術情報ありがとうございます。同じような事をやる際に試してみます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です