PHP-DB - 16. トランザクション管理
ここからはデータベースのトランザクション管理について学習します。トランザクションとはデータベースへの一連の処理をまとめた更新単位のことです。トランザクションを確定する場合にはコミット、トランザクションをキャンセルする場合にはロールバックを呼び出します。
PHPにおいては PDO クラスの beginTransaction メソッドを呼び出すことでトランザクションを開始します。また PDO クラスの commit メソッドを呼び出すことでトランザクションをコミットします。同様に PDO クラスの rollback メソッドを呼び出すことでトランザクションをロールバックします。
それではトランザクションの仕組みを理解するために次のプログラム( pdo14.php )を作成してみましょう。
<?php
$categories = [
    ["id" => 6, "title" => "Guitar"],
    ["id" => 7, "title" => "Piano"],
    ["id" => 7, "title" => "Drum"] // Invalid id.
];
try {
    $dsn = "sqlite:eldb.sqlite3";
    $username = null;
    $passwd = null;
    $pdo = new PDO($dsn, $username, $passwd);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    $pdo->beginTransaction();
    $sql = "insert into categories (id, title) values (:id, :title)";
    $ps = $pdo->prepare($sql);
    try {
        foreach ($categories as $category) {
            $ps->bindValue(":id", $category["id"], PDO::PARAM_INT);
            $ps->bindValue(":title", $category["title"], PDO::PARAM_STR);
            $ps->execute();
            echo "Insert: " . $category["title"] . PHP_EOL;
        }
        $pdo->commit();
    } catch (PDOException $e) {
        $pdo->rollBack();
        throw $e;
    }
} catch (PDOException $e) {
    echo $e->getMessage() . PHP_EOL;
}このプログラムでは先頭部分でデータベースに登録する3件のデータ( $categories 変数)を定義しています。
$categories = [
    ["id" => 6, "title" => "Guitar"],
    ["id" => 7, "title" => "Piano"],
    ["id" => 7, "title" => "Drum"] // Invalid id.
];コメントにもあるとおり、3件目のデータの "id" は 7 となっており、2件目のレコードの "id" と重複しているのがわかります。これからトランザクションを開始してこれら3件のレコードを順に登録しますが、3件目のレコードの登録で失敗することになります。
次に try ブロックの中で PDO インスタンスを生成し、 setAttribute メソッドを呼び出して必要な属性を設定しています。
    $dsn = "sqlite:eldb.sqlite3";
    $username = null;
    $passwd = null;
    $pdo = new PDO($dsn, $username, $passwd);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);ここでは PDO のエラーレポートの設定を変更して PDOException をスローするように変更し、 PDO のエミュレート機能も無効に変更しています。
次に PDO クラスの beginTransaction メソッドを呼び出して、トランザクションを開始しています。
    $pdo->beginTransaction();トランザクション開始以降に実行したSQLはコミットされるまでデータベースに反映されないようになります。またSQLの実行に失敗した場合は、ロールバックを呼び出すことでトランザクションをキャンセルできます。
以降のプログラムではプリペアドステートメントを作成し、先頭で定義した3件のデータ( $categories 変数)を繰り返し insert 文にバインドして実行しています。
    $sql = "insert into categories (id, title) values (:id, :title)";
    $ps = $pdo->prepare($sql);
    try {
        foreach ($categories as $category) {
            $ps->bindValue(":id", $category["id"], PDO::PARAM_INT);
            $ps->bindValue(":title", $category["title"], PDO::PARAM_STR);
            $ps->execute();
            echo "Insert: " . $category["title"] . PHP_EOL;
        }
        $pdo->commit();
    } catch (PDOException $e) {
        $pdo->rollBack();
        throw $e;
    }上記のプログラムでは try ブロックの最後の部分で $pdo->commit() を呼び出してトランザクションをコミットしています。エラーレポートの設定を変更して、SQLの実行時に予期せぬ自体が発生した場合は PDOException をスローするようにしているので、 try ブロックの最後の部分まで到達できた時点で実行したSQLはすべて成功していることになります。
もし try ブロックの処理の中で PDOException がスローされた場合、 処理は catch ブロックに移ります。 catch ブロックでは $pdo->rollBack() を呼び出してトランザクションをロールバックしています。
catchブロックの中でthrow $eと記述して再度PDOExceptionをスローしています。これは上位の(外側の)try - catch文にPDOExceptionをスローするためです。

それではコマンドラインからプログラムを実行してみましょう。
$ php pdo14.php
Insert: Guitar
Insert: Piano
SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: 
categories.idここではメッセージを折り返して表示しています。
実行結果から1件目( Giitar )、2件目( Piano )のレコードは登録できているものの、3件目のレコードで登録に失敗( PDOException )しているのがわかります。この場合、ロールバックを呼び出しているのでデータベースが更新されていないことを確認しておきましょう。
SQLite上で categories テーブルのレコードを表示します。
sqlite> select * from categories;
id          title
----------  -----------
1           Programming
2           Design
3           Marketing
4           Photo
5           Biz実行結果からトランザクションを正しくロールバックできていることがわかります。
トランザクションのコミット
それではトランザクションがコミットされる様子も確認しておきましょう。先ほどのプログラム( pdo14.php )の先頭のデータ定義を修正しましょう。
<?php
$categories = [
    ["id" => 6, "title" => "Guitar"],
    ["id" => 7, "title" => "Piano"],
    ["id" => 8, "title" => "Drum"],
];
try {
    $dsn = "sqlite:eldb.sqlite3";
    $username = null;
    $passwd = null;
    $pdo = new PDO($dsn, $username, $passwd);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    $pdo->beginTransaction();
    $sql = "insert into categories (id, title) values (:id, :title)";
    $ps = $pdo->prepare($sql);
    try {
        foreach ($categories as $category) {
            $ps->bindValue(":id", $category["id"], PDO::PARAM_INT);
            $ps->bindValue(":title", $category["title"], PDO::PARAM_STR);
            $ps->execute();
            echo "Insert: " . $category["title"] . PHP_EOL;
        }
        $pdo->commit();
    } catch (PDOException $e) {
        $pdo->rollBack();
        throw $e;
    }
} catch (PDOException $e) {
    echo $e->getMessage() . PHP_EOL;
}ここでは先頭部分の配列データを修正しています。
<?php
$categories = [
    ["id" => 6, "title" => "Guitar"],
    ["id" => 7, "title" => "Piano"],
    ["id" => 8, "title" => "Drum"]
];さきほどは "id" の値を意図的に重複させていましたが、今回の修正で "id" の値は重複は解消され、妥当なものとなります。
それではコマンドラインからプログラムを実行してみましょう。
$ php pdo14.php
Insert: Guitar
Insert: Piano
Insert: Drum実行結果から3件のレコードが正しく登録できていることがわかります。データベースに正しくコミットされているかを確認してみましょう。SQLiteで categories テーブルのレコードを取得します。
sqlite> select * from categories;
id          title
----------  -----------
1           Programming
2           Design
3           Marketing
4           Photo
5           Biz
6           Guitar
7           Piano
8           Drum実行結果からトランザクションを正しくコミットできていることがわかります。
まとめ
- PDOインスタンスの- beginTransactionメソッドを呼び出すとトランザクションを開始できる
- PDOインスタンスの- commitメソッドを呼び出すとトランザクションをコミットできる
- PDOインスタンスの- rollbackメソッドを呼び出すとトランザクションをロールバックできる