在用戶註冊初始化多張關聯表、問答系統同時寫入問題與回答等場景中,若部分 SQL 執行成功、部分失敗,將導致數據不一致(如用戶表已插入但權限表未建立)。MySQL 事務(Transaction)通過「全成功提交、一失敗回滾」特性完美解決此問題。本文封裝一個通用的批量 SQL 事務執行方法。
<?php
class Database {
private $conn;
public function __construct($host, $user, $pass, $dbname) {
$this->conn = new mysqli($host, $user, $pass, $dbname);
if ($this->conn->connect_error) {
die("連接失敗: " . $this->conn->connect_error);
}
$this->conn->set_charset('utf8mb4');
}
/**
* 批量執行 SQL 事務
* @param array $sqls SQL 語句陣列
* @return bool true: 全部成功提交, false: 已回滾
*/
public function executeTransaction(array $sqls): bool {
$this->conn->autocommit(false); // 關閉自動提交
$results = [];
try {
foreach ($sqls as $sql) {
$res = $this->conn->query($sql);
$results[] = $res !== false;
if (!$res) {
throw new Exception("SQL 執行失敗: {$this->conn->error} | SQL: $sql");
}
}
$this->conn->commit(); // 全部成功提交
return true;
} catch (Exception $e) {
$this->conn->rollback(); // 任一失敗回滾
error_log("事務回滾: " . $e->getMessage());
return false;
} finally {
$this->conn->autocommit(true); // 恢復自動提交
}
}
public function close() {
$this->conn->close();
}
}
// 使用範例
$db = new Database('localhost', 'root', 'password', 'my_db');
$sqls = [
"INSERT INTO users (username, email) VALUES ('john', 'john@example.com')",
"INSERT INTO profiles (user_id, bio) VALUES (LAST_INSERT_ID(), 'Hello')",
"INSERT INTO user_roles (user_id, role_id) VALUES (LAST_INSERT_ID(), 2)"
];
if ($db->executeTransaction($sqls)) {
echo "註冊成功,事務已提交";
} else {
echo "註冊失敗,已回滾";
}
$db->close();
autocommit(false) 開啟事務模式,MySQL 預設自動提交每條 SQL。try-catch 捕獲執行失敗,統一呼叫 rollback()。query() 返回 false 表示失敗,需顯式判斷。finally 區塊確保 autocommit(true) 執行,避免影響後續操作。防止 SQL 注入,建議改用 prepare() + bind_param():
public function executePreparedTransaction(array $statements): bool {
// $statements = [['sql' => 'INSERT ...', 'params' => [...], 'types' => 'ssi'], ...]
$this->conn->autocommit(false);
try {
foreach ($statements as $stmtData) {
$stmt = $this->conn->prepare($stmtData['sql']);
if ($stmtData['params']) {
$stmt->bind_param($stmtData['types'], ...$stmtData['params']);
}
if (!$stmt->execute()) {
throw new Exception($stmt->error);
}
$stmt->close();
}
$this->conn->commit();
return true;
} catch (Exception $e) {
$this->conn->rollback();
return false;
} finally {
$this->conn->autocommit(true);
}
}