トランザクションは再利用の敵である

Posted by Hiraku on 2013-10-22

釣りっぽいタイトル。「RDBのトランザクションが絡むとアプリケーション側のプログラムが書きにくくなる」という話です。

もちろんですが、RDBのトランザクション機能は偉大であり、Webアプリケーションでも意識して使わなければならず、「トランザクションなんて使うな」と言いたいわけではありません。

合成できない関数

PHPで素のPDOから考えます。たとえば、以下の関数に問題はあるでしょうか?

<?php
/*
 * 古いデータをアーカイブテーブルに移す関数のイメージ
 */
function moveDataToArchive(PDO $db) {
  $db->beginTransaction();
  try {
    $db->exec('
      INSERT INTO archives
             SELECT * FROM data
              WHERE published < CURRENT_DATE
    ');
    $db->exec('
      DELETE data
       WHERE published < CURRENT_DATE
    ');

    $db->commit();

  } catch (Exception $e) {
    $db->rollback();
    throw $e;
  }
}

SQLの中身はどうでもいいんですが、こんな風にbeginTransactionとrollbackを丸ごと含めた関数を作ることは多いのではないでしょうか。しかしこの書き方は悪手です!

この関数と一緒に何か別のSQLを組み合わせたくなったとき、その新たなSQLがトランザクションの中に含められないことに気づくことになります。

要するに、トランザクションは合成できないのです。

<?php
function hoge(PDO $db) {
  $db->beginTransaction();
  //…
  $db->commit();
}

function fuga(PDO $db) {
  $db->beginTransaction();
  //…
  $db->commit();
}

//こういう関数は作れない!!(実行するとエラーが発生する)
function hogeAndFuga(PDO $db) {
  $db->beginTransaction();
  hoge($db);
  fuga($db);
  $db->commit();
}

トランザクション込みで関数を作ってしまうと、その時点で再利用性はできるものの、合成できないので使いにくい関数になってしまいます。

解決策は?

ぱっと思いつくのが二通りあります。ライブラリを変えるか、書き方を変えるか。

  • ライブラリを工夫して上記の書き方でも問題が起きないようにする
  • 書き方を工夫する。徹底的にトランザクションを書かないようにする

ライブラリでカバーする方法

beginTransaction()を入れ子にすることができないのは、PDOがショボいせいだ!という意見もあるでしょう。例えばbeginTransaction()を呼び出した回数を状態として保持しておくなど、ちょっと工夫すればこの問題は解決できます。

DoctrineのDB抽象化レイヤー(DBAL)にはこの機能があるようで、トランザクションが入れ子になっても問題なく動作します。

<?php
// $conn instanceof Doctrine\DBAL\Connection
$conn->beginTransaction(); // 0 => 1, "real" transaction started
try {

    ...

    // nested transaction block, this might be in some other API/library code that is
    // unaware of the outer transaction.
    $conn->beginTransaction(); // 1 => 2
    try {
        ...

        $conn->commit(); // 2 => 1
    } catch (Exception $e) {
        $conn->rollback(); // 2 => 1, transaction marked for rollback only
        throw $e;
    }

    ...

    $conn->commit(); // 1 => 0, "real" transaction committed
} catch (Exception $e) {
    $conn->rollback(); // 1 => 0, "real" transaction rollback
    throw $e;
}

入れ子が解消した瞬間に実際のコミットがされるよう、調整がされているのです。

「Doctrineすばらしい」「みんなDoctrineを使えばいい」「生PDO使ってる奴は情弱www」となりそうなところですが、うーん、残念ながらまだ考察が足りていません。

入れ子にできるからと言って、こうbeginTransaction()〜commit()のブロックがあちこちに登場することになると、全然DRY(Don't Repeat Yourself)じゃない!という状態になります。こういうのはアスペクト指向(AOP)で解決したいところですが、AOPライブラリがないと生きていけないというのも本質的ではないと思います。

また、トランザクションには分離レベルに示される種類があります。パフォーマンスは落ちるけど起きる問題が少ないレベルと、起きる問題が多いけれどパフォーマンスが上がるレベルを4段階に分け、クライアントプログラムから制御できるようにしています。

ブロックのコピペに我慢できたとしても、トランザクション分離レベルの制御はお手上げです。合成したトランザクションはどの分離レベルで動作するべきなのでしょうか? おそらく合成したトランザクションのうち、最も厳しいレベルに合わせるべきだと思いますが、通常の関数合成では、「最も厳しいレベル」を検知するのが困難です。

書き方でカバーする方法

ライブラリでカバーしないのなら、書き方もしくは設計でカバーするしかありません。単純なのは、トランザクションのブロックを関数内に書かないようにすることです。

<?php

//合成できる関数
function hoge(PDO $pdo)
{
  return $pdo->query(/* ... */);
}

//合成できる関数
function fuga(PDO $pdo, $param)
{
  return $pdo->query(/* ... */);
}

//使用するときはトランザクションブロックを忘れずに書く
//ただしこの関数は合成できなくなった
function hogeAndFuga(PDO $pdo)
{
  try {
    $pdo->beginTransaction();
    $results = hoge($pdo);
    fuga($pdo, $results);
    $pdo->commit();
  } catch (Exception $e) {
    $pdo->rollback();
    throw $e;
  }
}

解決はしていますが、あくまで紳士協定のような方法なので、合成用に用意した関数はうっかり素のままで呼ぶと危険です。「この関数はトランザクション中でないと呼べない」と表明する手段があればいいのですが。。PDO::inTransaction()とかでチェックするのも面倒ですし、トランザクション中を表す状態ごとのPDO派生型を作ればいいのかな。

根本解決方法はないの?

ライブラリによる解決方法、設計による解決方法、いずれも完璧かと言われると微妙なところです。もっといいやり方はないのでしょうか?

そもそも、どうしてトランザクションは合成できないのでしょうか。これは、トランザクションがテーブルロックを元に想定された概念であり、「ロックを用いたプログラムは原理的に合成できない」ことに由来するのではないかと考えています。

ならば、「トランザクションが絡んだプログラムは何故か上手く書けない」問題は、プログラマの技術力や設計の拙さにあるのではなく、もっと根本的な、本質的に困難な課題だと言うことになります。

モデルが原因ならば、根本解決にはモデルを変えるしかありません。ロックを使わず、合成可能な別の同時実行制御モデルをベースにDSLを組み上げ、その内部で自動的にトランザクションを組み立てるような大掛かりなラッパーを作らなければ根本解決にはならないでしょう。

そこまでのライブラリを組み立てる根性がなければ、ひとまずは、トランザクションを扱う際は気を付けて書きましょう、ぐらいになるでしょうか。

まとめ

  • トランザクション込みで関数化すると再利用性が落ちるという問題があります。
  • 私には対処方法がまだ見えていないので、いいやり方があったら教えてください。

SOAとかやってれば似たような問題には割とすぐ行き着くと思うんですが、皆さんどんなふうに解決してるんですかね。。

PHPの最新記事