使用者註冊、搜尋關鍵字記錄、表單提交等場景,常面臨「避免重複資料寫入」的需求。早期常見做法是「先查再插」,但存在競態條件與效能問題。
<?php
// 經典反面教材
$username = $_GET['name'] ?? '';
$q = "SELECT * FROM usertable WHERE user_name = '$username'";
$rs = mysql_query($q);
if (mysql_num_rows($rs) === 0) {
mysql_query("INSERT INTO student (user_name) VALUES ('$username')");
echo "註冊成功";
} else {
echo "用戶名已存在";
}
缺點:
mysql_* 函式已移除(PHP 7.0+)<?php
// 需先建立 UNIQUE INDEX
$pdo->exec("CREATE UNIQUE INDEX uk_username ON usertable(user_name)");
// 寫入
$stmt = $pdo->prepare("INSERT IGNORE INTO usertable (user_name) VALUES (?)");
$stmt->execute([$username]);
if ($stmt->rowCount() > 0) {
echo "新增成功";
} else {
echo "已存在";
}
優點: 原子性、單次往返、無競態。
缺點: INSERT IGNORE 會吞沒其他錯誤(如外鍵違反),除錯困難。
<?php
try {
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// 確保唯一索引存在(遷移腳本執行一次即可)
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS uk_username ON usertable(user_name)");
$stmt = $pdo->prepare("INSERT INTO usertable (user_name) VALUES (?)");
$stmt->execute([$username]);
echo "註冊成功";
} catch (PDOException $e) {
if ($e->getCode() === '23000') { // SQLSTATE 23000: integrity constraint violation
echo "用戶名已存在";
} else {
error_log("DB Error: " . $e->getMessage());
echo "系統錯誤,請稍後再試";
}
}
| 層面 | 建議 |
|---|---|
| 資料庫層 | 務必加 UNIQUE INDEX,讓 DB 保證資料完整性 |
| 連線層 | 使用 PDO,啟用 ERRMODE_EXCEPTION |
| 查詢層 | 全部使用 預備語句,防 SQL 注入 |
| 錯誤層 | 捕獲 PDOException,依 SQLSTATE 碼區分業務邏輯 |
| 編碼層 | 連線指定 charset=utf8mb4,支援完整 Unicode |
$stmt = $pdo->prepare("
INSERT INTO usertable (user_name) VALUES (?)
ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)
");
$stmt->execute([$username]);
$userId = $pdo->lastInsertId(); // 無論新增或已存在,都能拿到 ID
echo "用戶 ID: $userId";
「先查後插」是反模式。正確做法:資料庫加唯一索引 + 預備語句 + 捕獲唯一鍵衝突例外。這樣既解決併發安全,又簡化程式碼,符合現代 PHP 開發標準。