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 = "mysql:host=localhost;dbname=eldb;charset=utf8mb4";
$username = "root";
$password = "admin";
$pdo = new PDO($dsn, $username, $password);
$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 = "mysql:host=localhost;dbname=eldb;charset=utf8mb4";
$username = "root";
$password = "admin";
$pdo = new PDO($dsn, $username, $password);
$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
)しているのがわかります。この場合、ロールバックを呼び出しているのでデータベースが更新されていないことを確認しておきましょう。
MySQL上で categories
テーブルのレコードを表示します。
MariaDB [eldb]> select * from categories order by id;
+----+-------------+
| id | title |
+----+-------------+
| 1 | Programming |
| 2 | Design |
| 3 | Marketing |
| 4 | Photo |
| 5 | Biz |
+----+-------------+
5 rows in set (0.00 sec)
MariaDB [eldb]>
実行結果からトランザクションを正しくロールバックできていることがわかります。
トランザクションのコミット
それではトランザクションがコミットされる様子も確認しておきましょう。先ほどのプログラム( pdo14.php
)の先頭のデータ定義を修正しましょう。
<?php
$categories = [
["id" => 6, "title" => "Guitar"],
["id" => 7, "title" => "Piano"],
["id" => 8, "title" => "Drum"],
];
try {
$dsn = "mysql:host=localhost;dbname=eldb;charset=utf8mb4";
$username = "root";
$password = "admin";
$pdo = new PDO($dsn, $username, $password);
$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件のレコードが正しく登録できていることがわかります。データベースに正しくコミットされているかを確認してみましょう。MySQLで categories
テーブルのレコードを取得します。
MariaDB [eldb]> select * from categories order by id;
+----+-------------+
| id | title |
+----+-------------+
| 1 | Programming |
| 2 | Design |
| 3 | Marketing |
| 4 | Photo |
| 5 | Biz |
| 6 | Guitar |
| 7 | Piano |
| 8 | Drum |
+----+-------------+
8 rows in set (0.00 sec)
MariaDB [eldb]>
実行結果からトランザクションを正しくコミットできていることがわかります。
まとめ
PDO
インスタンスのbeginTransaction
メソッドを呼び出すとトランザクションを開始できるPDO
インスタンスのcommit
メソッドを呼び出すとトランザクションをコミットできるPDO
インスタンスのrollback
メソッドを呼び出すとトランザクションをロールバックできる