PHPとMySQLでトランザクションを実装する方法
はじめに
トランザクションは、データベースの処理において一連の操作をまとめて実行し、成功するか失敗するかのいずれかで全体が処理される仕組みです。PHPとMySQLを使ってお金の残高を例にトランザクションを実装する方法を学んでみます。
前提
なにかしらバックエンド言語の基礎とDB基礎は学習済みであること。
トランザクションとは?
トランザクション(英:transaction)は、データベース操作のまとまりを指します。複数のクエリを一つの処理単位とし、全体が成功するか失敗するかのいずれかで処理されます。トランザクションにはACIDと呼ばれる特性があります。
- 原子性 (Atomicity): トランザクションは不可分の操作単位として扱われます。全ての操作が成功するか、全てが失敗するかのどちらかです。
- 一貫性 (Consistency): トランザクションの前後ではデータベースの一貫性が保たれます。
- 分離性 (Isolation): トランザクションは互いに干渉せず、個別に処理されるため、並行処理の際にデータの整合性が保たれます。
- 永続性 (Durability): トランザクションが成功した場合、その結果はデータベースに永続的に保存されます。
事前準備
usersテーブルの作成:
CREATE TABLE users (
id INT(11) NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
PRIMARY KEY (id)
)ENGINE=InnoDB;
- id: ユーザーの一意の識別子として使用される整数型の列。
- name: ユーザーの名前を格納するための文字列型の列。
- email: ユーザーのメールアドレスを格納するための文字列型の列。
accountsテーブルの作成:
CREATE TABLE accounts (
id INT(11) NOT NULL AUTO_INCREMENT,
user_id INT(11) NOT NULL,
balance DECIMAL(10,2) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (user_id) REFERENCES users(id)
)ENGINE=InnoDB;
- id: アカウントの一意の識別子として使用される整数型の列。
- user_id: アカウントに紐づくユーザーの識別子を格納するための整数型の列。usersテーブルのid列と関連付けられています。
- balance: アカウントの残高を格納するための固定小数点数型の列。
注意:トランザクションを利用する際にデータベースのストレージエンジンはInnoDBである必要があります。InnoDBはMySQL 5.5以降のバージョンでデフォルトのストレージエンジンとなっておりますが、レガシーな環境の場合はデフォルトのストレージエンジンがMyISAMになっている場合がありますので確認するようにしましょう。今回はCREATE分でENGINE=InnoDBと指定しております。
テストデータの作成
INSERT INTO users (name, email) VALUES ('Taro Yamada', '[email protected]');
INSERT INTO accounts (user_id, balance) VALUES (1, 1000); -- 初期残高1000円
PHPでトランザクションを実装する方法
PHPには、MySQLデータベースを操作するための多くの手法がありますが、ここではPDO(PHP Data Objects)を使用した例を紹介します。
まず、データベースへの接続を確立します。
<?php
$dsn = 'mysql:host=localhost;dbname=mydatabase';
$username = 'username';
$password = 'password';
try {
$pdo = new PDO($dsn, $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
echo '接続エラー:' . $e->getMessage();
exit();
}
次に、トランザクションを開始し、残高を足したり減らしたりする複数のクエリを実行します。
<?php
try {
$pdo = new PDO($dsn, $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->beginTransaction();
// トランザクション内でのクエリ実行
$pdo->exec("UPDATE accounts SET balance = balance - 100 WHERE user_id = 1");
$pdo->exec("UPDATE accounts SET balance = balance + 200 WHERE user_id = 1");
// トランザクションのコミット
$pdo->commit(); echo "トランザクションが正常に完了しました。";
} catch (PDOException $e) {
// エラー発生時の処理
$pdo->rollBack();
echo "トランザクション中にエラーが発生しました:" . $e->getMessage();
}
この場合トランザクションが正常に完了してコミット(英:commit)されたことにより残高が1100円になっているはずです。
このコミットは処理が一つでも失敗されると実行されません。
では次は、わざとクエリーを間違えてみます。
<?php
try {
$pdo = new PDO($dsn, $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->beginTransaction();
// トランザクション内でのクエリ実行
// 正常なクエリー
$pdo->exec("UPDATE accounts SET balance = balance + 200 WHERE user_id = 1");
// フィールド名をわざと間違えてた異常なクエリー(balanseになっています)
$pdo->exec("UPDATE accounts SET balanse = balanse + 200 WHERE user_id = 1");
// トランザクションのコミット
$pdo->commit(); echo "トランザクションが正常に完了しました。";
} catch (PDOException $e) {
// エラー発生時の処理
$pdo->rollBack();
echo "トランザクション中にエラーが発生しました:" . $e->getMessage();
}
実行してみると、残高が1100円から結果が変わっていませんよね。どれかいずれかでも失敗したときに元に戻すことをロールバック(英:rollback)と言います。では、もしもトランザクションを使わない場合はどうなってしまうのかを見てみましょう。
<?php
try {
$pdo = new PDO($dsn, $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 正常なクエリー
$pdo->exec("UPDATE accounts SET balance = balance + 200 WHERE user_id = 1");
// フィールド名をわざと間違えてた異常なクエリー(balanseになっています)
$pdo->exec("UPDATE accounts SET balanse = balanse + 200 WHERE user_id = 1");
} catch (PDOException $e) {
echo "エラーが発生しました:" . $e->getMessage();
}
先に実行された正常なクエリーの方だけが結果に反映されてしまい、残高が200円増えて1300円になってしまってます。これではデータの整合性が取れないです。
以上のようにトランザクション内の複数の操作が実行される場合、トランザクションは「すべての操作が正常に完了するか、どれか一つでも失敗してなかったことになるか」のいずれかの状態で終了します。
処理の流れは図の通りです。トランザクションはBEGINで始まり、COMMITまたはROLLBACKで終了します。
ここまで、トランザクションを実装する方法とトランザクションは何かについてお金の残高を例に学びました。トランザクションは、データベース操作における信頼性、整合性、並行性を確保するために非常に重要な機能です。特に複数の操作をまとめて処理する場合や、データの一貫性が重要な場合には、トランザクションの利用が推奨されます。ぜひ覚えておきましょう。
まとめ
・トランザクションとはデータベース操作のまとまり、一つの処理単位のこと
・ストレージエンジンはInnoDBで使用が可能
・トランザクションは
「すべての操作が正常に完了するか、どれか一つでも失敗してなかったことになるか」で終了する