CakePHPで複数テーブルに対するトランザクションを使う場合

CakePHPトランザクションを使用する必要があったのですが、一般的に用いられている方法だと、複数のテーブルを1つのトランザクションとして更新したい場合、コントローラ内での実装がとても分かりにくくなると感じ、異なる実装方法をとってみたので、ご紹介します。

一般的な実装方法とその課題

一般的な実装方法としては、app/models/app_model.phpに、下記のようなトランザクション管理用のメソッドを追加することが多いと思います。基本的に、各モデルクラスは、AppModelクラスを継承しているため、これらのメソッドをどのモデルからも利用可能になります。

function begin() {
    $db = & ConnectionManager::getDataSource($this->useDbConfig);
    $db->begin($this);
}
function commit() {
    $db = & ConnectionManager::getDataSource($this->useDbConfig);
    $db->commit($this);
}
function rollback() {
    $db = & ConnectionManager::getDataSource($this->useDbConfig);
    $db->rollback($this);
}


しかし、この方法では、各モデルクラスのトランザクション管理用メソッドを利用するため、下記の例のようになります。

$this->User->begin();
$this->User->save($userData);
$this->Company->save($companyData);
$this->User->commit();

どうでしょうか?上記の実装を見て、Userモデルと、Companyモデルのトランザクションの関係を判断できますか?
答えは、Userモデルと、Companyモデルが同じDB接続設定(database.phpの設定)を利用していれば、同じトランザクションとなります。*1
ですが、そうは見えませんよね?これは混乱の元ですし、不具合につながる危険性もあります。

トランザクションの管理をコントローラ層に

コントローラ層でトランザクション境界を明確にする目的で、コンポーネントとして新たに「Transactionクラス」を用意します。
beginメソッドだけ抜粋して載せておきます。この実装であれば、もし、Userモデルと、Companyモデルが異なる接続先設定を参照していたとしても、基本的には問題なく動作します。*2

public function begin($models) {
    // インスタンス変数へ格納⇒commit,rollback時に利用する
    $this->models = $models;

    //各モデル毎にトランザクションを開始
    foreach ($models as $model) {
        if (!$this->_begin($model)) {
            //トランザクション開始失敗時の処理
            break;
        }
    }
}

※$this->_beginの中で、各モデルの接続先単位でトランザクション開始するように実装されるイメージ。


コントローラでの実装は以下のようになります。このように、Userモデルと、Companyモデルが同じトランザクションにいるということがハッキリします。

$this->Transaction->begin(array($this->User,$this->Company));

$user = $this->User->save($userData);
$company = $this->Company->save($companyData);

if($user !== false && $company !== false){
  /*
     * Transactionコンポーネントの内部実装としては、
     * ・両モデルが同じ接続先:1度だけcommitが実行される
     * ・両モデルが異なる接続先:2つのDBにcommitが実行される
     */
    $this->Transaction->commit();
}else{
    // commitと同様の動作
    $this->Transaction->rollback();
}

*1:CakePHPではDB接続設定(database.phpの設定)の単位でコネクションを使い回すため、こうなります。言い換えれば、Userモデルと、Companyモデルが異なる接続先設定を参照していれば、違うトランザクションとなります。

*2:異なる接続先設定の場合、厳密には2フェーズコミットとして処理しなければなりませんが、ここではそこまで厳密なコードにはなっていません。