初心者必見！PHPとMySQLでのトランザクション処理でよくある失敗と解決策!!（修正版）

## はじめに

データベースのトランザクションは、複数の操作を1つの単位として扱い、処理の一貫性を保つために非常に重要な役割を担います。  
特に、複数のデータベース操作を一度に行う場合、エラー発生時に適切にロールバックできることが求められます。

本記事では、PHPとMySQLを使用したトランザクション処理において初心者がつまずきやすいポイントとその回避方法を解説します。

## 対象読者

- PHPを使っている開発者  
- MySQLを使用している開発者  
- トランザクション管理の基本を学びたい方  
- 既存のコードでトランザクション周りに問題が発生している方  

## 事前準備

本記事は以下の環境を前提としています：

- PHP 8.x  
- MySQL 8.x  
- MySQLiまたはPDO拡張が有効化されていること  

## トランザクションの基本

データベースのトランザクションは、次の4つのACID特性を持ちます：

1. **Atomicity（原子性）**  
2. **Consistency（整合性）**  
3. **Isolation（独立性）**  
4. **Durability（永続性）**  

PHP（PDO）でトランザクションを使う場合は、以下の手順を踏みます：

1. トランザクション開始  
2. 複数のデータ操作  
3. コミットまたはロールバック  

### トランザクションの基本コード（PDO）

```php
<?php
try {
    $pdo = new PDO('mysql:host=localhost;dbname=test', 'root', 'password');
    
    // トランザクション開始
    $pdo->beginTransaction();

    // SQLの実行
    $stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
    $stmt->execute(['test taro', 'taro@example.com']);

    // コミット
    $pdo->commit();
} catch (PDOException $e) {
    // トランザクション中であればロールバック
    if ($pdo && $pdo->inTransaction()) {
        $pdo->rollBack();
    }

    // 本番環境ではエラーをログに記録
    error_log("エラー: " . $e->getMessage());
    echo "エラーが発生しました。管理者にお問い合わせください。";
}
?>
````

---

## 初心者がつまずきやすいポイント

### 1. ❌【誤り】トランザクション開始前の自動コミット設定

```php
$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, false); // 自動コミットを無効化
```

このコードは **PDOでは動作しません。**
`PDO::ATTR_AUTOCOMMIT` は一部ドライバではサポートされておらず、MySQLのPDOでは無視されます。

✅ **正しい方法：`beginTransaction()` を使う**

```php
$pdo->beginTransaction();
```

このメソッドを呼び出すことで、PDOが自動的にオートコミットを無効にします。

---

### 2. コミットとロールバックのタイミングミス

すべての操作が成功した後に `commit()` を行い、例外が発生した場合は `rollBack()` を忘れずに行いましょう。
また、`rollBack()` を呼ぶ際には `inTransaction()` でチェックするとより安全です。

---

### 3. ロールバックを忘れる

`catch` ブロック内で `rollBack()` を忘れずに記述しましょう。
※ `beginTransaction()` を呼ばなかった場合や、既にロールバックされた場合を考慮して `inTransaction()` を使うのがベストです。

```php
if ($pdo->inTransaction()) {
    $pdo->rollBack();
}
```

---

### 4. トランザクション分離レベルを考慮しない

```php
$pdo->exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
```

分離レベルは、**`beginTransaction()` の直前に設定する必要があります。**
既にトランザクションが開始された後に設定しても効果がありません。

#### 🔰 補足：トランザクション分離レベルって何？

トランザクション分離レベルとは、**複数のトランザクションが同時にデータベースへアクセスする際、互いの影響をどの程度許容するか**を決めるルールです。
この設定によって、以下のような問題を防ぐことができます：

| 問題          | 内容                          |
| ----------- | --------------------------- |
| ダーティリード     | 他のトランザクションの未確定データを読み取ってしまう  |
| ノンリピータブルリード | 同じクエリを実行したのに、別の結果が返ってくる     |
| ファントムリード    | 条件に合致するレコードの数がトランザクション中に変わる |

MySQLのInnoDBエンジンでは、以下の4つの分離レベルが用意されています。
（下に行くほど厳密）

| 分離レベル            | 特徴                      | 備考                 |
| ---------------- | ----------------------- | ------------------ |
| READ UNCOMMITTED | 最も緩い。ダーティリードを許す         | テスト以外では非推奨         |
| READ COMMITTED   | コミット済みのデータのみ読める         | PostgreSQLのデフォルト   |
| REPEATABLE READ  | 同じトランザクション中の読み取りは常に同じ結果 | MySQLのデフォルト        |
| SERIALIZABLE     | 全てのトランザクションを直列化して実行     | 同時実行性が大きく低下する可能性あり |

#### いつどれを使う？

トランザクション分離レベルの指定については、以下の内容を参考にしてみてください😼

* ✅ **`REPEATABLE READ`（MySQLのデフォルト）**

  * **標準的で安全性とパフォーマンスのバランスが良い**
  * **ユースケース例**
    ユーザー情報の更新や注文処理など、一連の処理中にデータが変わらないことを保証したい場合。
    例えば、ショッピングカートの内容を確認しながら購入処理を行うECサイトでは、処理中にカート内容が変わってしまうとりべんせが悪くなるため、この分離レベルが適しています。

* ✅ **`READ COMMITTED`**

  * **他のトランザクションと協調しやすい。変更を即時に反映したいケースで有用**
  * **ユースケース例**
    チャットアプリやリアルタイムダッシュボードなど、最新の情報をできるだけ早く反映したい場合。
    例えば、在庫管理システムで他の担当者が更新した在庫数を素早く確認したい時に適しています。

* ❌ **`READ UNCOMMITTED`**

  * **基本的に使わない**
  * **理由**
    ダーティリード（未確定のデータを読み取る）を許してしまい、データの整合性が大幅に崩れる可能性があるためです。
  * **ユースケース例（ほぼなし）**
    テストや検証環境で動作確認をする場合のみ限定的に利用されることがあります。

* ⚠️ **`SERIALIZABLE`**

  * **厳密な一貫性が必要な場合のみ（競合やパフォーマンスへの影響に注意）**
  * **ユースケース例**
    銀行の口座振替や決済処理など、絶対にデータの競合や二重支払いがあってはならないクリティカルなトランザクション。
    この分離レベルは並列実行を制限するため、処理速度が大幅に落ちる可能性があることを考慮して使います。

#### 設定の例

```php
// セッション単位で分離レベルを設定し、次のトランザクションに適用する例
$pdo->exec("SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ");
$pdo->beginTransaction();
```

---

#### 🔰 補足：セッション全体の分離レベルを変更したい場合

```php
$pdo->exec("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
```

こちらは現在のセッションで実行するすべてのトランザクションに適用されます。
ただし、アプリケーション全体の動作に影響を及ぼすため、変更時は注意してください。

---

### 5. 複数の接続をトランザクション中に使わない

トランザクションは **コネクション単位で管理されます。**

複数のPDOインスタンス（コネクション）で処理を分けると、トランザクションの意味がなくなり、データの整合性が崩れます。

---

### 6. トランザクションを長時間開いたままにしない

トランザクションがロックを保持し続けるため、長時間放置するとデッドロックやパフォーマンス劣化の原因になります。
可能な限り、短時間で処理を完結させましょう。

---

## まとめ

* `PDO::ATTR_AUTOCOMMIT` はMySQL PDOでは無視されるので使わない
* `beginTransaction()` でトランザクション開始、例外発生時は必ずロールバック
* トランザクション分離レベルは `beginTransaction()` の直前に設定する
* 複数接続でトランザクションをまたがない
* トランザクションは短時間で処理を終える
* エラーは `try-catch` で捕捉し、ログ記録も忘れずに

PHP＋MySQLでのトランザクション処理は非常に強力ですが、初心者がつまずきがちなポイントも多く存在します。
本記事で紹介したポイントを参考に、トランザクションの開始、コミット、ロールバックを正しく実装することで、データベースの整合性を保ちながら安定したシステムを構築することができます:smirk_cat:

## 謝辞
~~1. トランザクション開始前の自動コミット設定~~
~~トランザクションを使用する際には、最初に自動コミットを無効にする必要があります。~~

@YuneKichi さん、誤りをご指摘いただきありがとうございました:bow:

---

## 参考リンク

* [PHP: PDO::beginTransaction - Manual](https://www.php.net/manual/ja/pdo.begintransaction.php)
* [MySQL 8.0 Reference Manual - Transaction Isolation Levels](https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html)
* [MySQL Transaction Model](https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-model.html)