MyBatis 執行 INSERT 時因主鍵重複(Duplicate entry 'xxx' for key 'PRIMARY')拋出 PersistenceException / MySQLIntegrityConstraintViolationException,導致業務流程中斷、事務回滾。
| 方案 | 核心邏輯 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| 方案一:異常捕獲轉換 | try-catch 捕獲異常,解析錯誤碼/訊息,返回友好提示 | 代碼簡潔、無額外查詢、併發安全(資料庫鎖保證) | 依賴特定資料庫錯誤碼、異常處理有性能開銷 | 低併發、主鍵衝突極少見、業務允許「嘗試插入失敗則提示」 |
| 方案二:預查詢判斷 | 先 SELECT id FROM table WHERE id = ?,不存在再 INSERT | 邏輯直觀、可自定義衝突處理(更新/忽略/報錯) | **併發不安全**(TOCTOU 競態條件)、雙倍查詢開銷 | 單線程/低併發、需區分「新增」與「更新」邏輯 |
| 方案三:資料庫原生 UPSERT(推薦) | INSERT ... ON DUPLICATE KEY UPDATE (MySQL) / MERGE (Oracle/SQL Server) / ON CONFLICT (PostgreSQL) | **原子操作、併發安全、單次交互、效能最高** | 語法因資料庫異、需處理「更新哪些欄位」邏輯 | **所有生產環境首選** |
<insert id="insertOrUpdateUser" parameterType="User">
INSERT INTO t_user (id, username, email, create_time, update_time)
VALUES (#{id}, #{username}, #{email}, NOW(), NOW())
ON DUPLICATE KEY UPDATE
username = VALUES(username),
email = VALUES(email),
update_time = NOW()
</insert>
<insert id="insertOrUpdateUser" parameterType="User">
INSERT INTO t_user (id, username, email, create_time, update_time)
VALUES (#{id}, #{username}, #{email}, NOW(), NOW())
ON CONFLICT (id) DO UPDATE SET
username = EXCLUDED.username,
email = EXCLUDED.email,
update_time = NOW()
</insert>
<insert id="insertOrUpdateUser" parameterType="User">
MERGE INTO t_user t
USING (SELECT #{id} AS id FROM dual) s
ON (t.id = s.id)
WHEN MATCHED THEN UPDATE SET
t.username = #{username},
t.email = #{email},
t.update_time = SYSDATE
WHEN NOT MATCHED THEN INSERT (id, username, email, create_time, update_time)
VALUES (#{id}, #{username}, #{email}, SYSDATE, SYSDATE)
</insert>
@Service
public class UserService {
@Autowired UserMapper userMapper;
public Result<Void> insertUser(User user) {
try {
userMapper.insert(user);
return Result.success();
} catch (PersistenceException e) {
Throwable cause = e.getCause();
if (cause instanceof MySQLIntegrityConstraintViolationException) {
String msg = cause.getMessage();
if (msg != null && msg.contains("Duplicate entry")) {
return Result.error("主鍵已存在:" + user.getId());
}
}
throw e; // 非主鍵衝突異常向上拋出
}
}
}
public Result<Void> insertUserSafe(User user) {
// 併發風險:此處與 INSERT 之間有時間窗口
if (userMapper.selectById(user.getId()) != null) {
return Result.error("主鍵已存在");
}
userMapper.insert(user);
return Result.success();
}
採用 UPSERT 重構後,主鍵衝突不再拋出異常,而是自動轉為更新或忽略,徹底解決插入異常導致業務中斷問題。