想像一下,你正在寫一個網路商店的應用程式,在訂單付款時你需要:
- 從客戶的信用卡扣款。
- 減少倉庫裡商品的數量。
- 建立一筆成功交易的紀錄。
如果這些動作中間有什麼出錯怎麼辦?比如說,錢已經扣了,但商品在扣完錢後才發現沒庫存,還沒來得及建立訂單紀錄?一切都亂套啦:錢「卡住」了,訂單沒完成,你的 server 收到一堆憤怒的 email(甚至可能還有律師信)。
transaction 就是為了避免這種情況而生的。它們讓你可以把多個操作包成一個「原子」單位來跟資料庫互動。就像文字編輯器裡的「Undo」按鈕:如果哪裡出錯了,直接回到一開始就好。
transaction 怎麼確保資料完整性?
transaction 的核心就是 ACID 這個概念:
- 原子性 (Atomicity) — transaction 裡的所有操作要嘛全部做完,要嘛一個都不做。「全有或全無」。
- 一致性 (Consistency) — transaction 前後,資料都要保持一致的狀態。
- 隔離性 (Isolation) — 一個 transaction 不會干擾到其他 transaction。
- 持久性 (Durability) — transaction 完成後,即使系統掛掉,結果也會被保存下來。
為什麼我又要重複這個?因為這是大家追求的理想狀態。然後……其實很難完全做到。等我們課程後面再回來講 transaction,你就會發現有些 ACID 原則我們還是得妥協。
所以現在先享受一下 transaction 看起來很簡單又美好的時光吧。來,直接看範例!
transaction 的使用範例
來看一個加學生並讓他註冊課程的情境。
假設我們在一個大學的資料庫工作。最近有一些旁聽生想上我們的課。如果課程還有名額,我們就把這個旁聽生暫時註冊成學生,然後加到課程裡。流程大概是這樣:
新增學生並註冊課程時,我們需要:
- 在
students表新增一筆紀錄。 - 在
enrollments表新增一筆,把學生跟課程連起來。
如果中間有什麼出錯(比如課程已經滿了),我們就要把操作全部撤銷,避免資料表之間不一致。做法如下:
-- 開始 transaction
BEGIN;
-- 步驟 1: 新增學生
INSERT INTO students (name, age, gender)
VALUES ('Otto Lin', 20, 'Male')
RETURNING id;
-- 假設回傳 id = 10
-- 步驟 2: 幫他註冊課程
INSERT INTO enrollments (student_id, course_id)
VALUES (10, 5);
-- 都成功了?就提交
COMMIT;
如果出錯會怎樣?
假如註冊課程時出錯:比如課程不存在。如果你忘了用 transaction,students 表會留下學生紀錄,但 enrollments 表沒有對應資料。這樣資料就亂掉了。為了避免這種情況,我們可以用 ROLLBACK 指令。
-- 開始 transaction
BEGIN;
-- 步驟 1: 新增學生
INSERT INTO students (name, age, gender)
VALUES ('Otto Lin', 20, 'Male')
RETURNING id;
-- 步驟 2: 嘗試註冊課程
INSERT INTO enrollments (student_id, course_id)
VALUES (10, 999); -- 錯誤:id = 999 的課程不存在!
-- 撤銷所有變更
ROLLBACK;
這樣一來,所有操作都不會生效,資料庫會回到 transaction 開始前的狀態。
用 SAVEPOINT 控制流程
現在想像一個更複雜的情境。你要做很多操作,但只想撤銷到某個特定點,而不是全部重來。
來做一個分步註冊學生的例子:
-- 開始 transaction
BEGIN;
-- 新增學生
SAVEPOINT add_student; -- 建立儲存點
INSERT INTO students (name, age, gender)
VALUES ('Anna Song', 22, 'Female');
-- 註冊第一門課
SAVEPOINT enroll_course_1; -- 再一個儲存點
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 5);
-- 註冊第二門課(這裡出錯)
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 999); -- 錯誤!
-- 只撤銷到最後一個儲存點
ROLLBACK TO enroll_course_1;
-- 繼續流程
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 6);
-- 提交變更
COMMIT;
這樣一來,流程某一部分出錯也不會影響其他已經成功的操作。
怎麼檢查有沒有真的改到資料
如果 SQL 指令有改到資料,你可以檢查到底有沒有真的動到東西。
比如你執行 DELETE,但 WHERE 條件沒選到任何資料。或是 UPDATE,但資料早就改過了,實際上什麼都沒變。
這時可以用一個系統變數 FOUND。它會告訴你上一個 SQL 指令有沒有動到資料:
FOUND = TRUE— 有更新/刪除到資料;FOUND = FALSE— 沒有刪除或改到任何東西。
普通的 SELECT 不會用到這個,只有追蹤資料變更時才有用。
實戰:處理付款
transaction 在金融應用特別重要。再來看一個把錢從一個帳戶轉到另一個帳戶的例子。
-- 開始 transaction
BEGIN;
-- 步驟 1: 從第一個帳戶扣錢
UPDATE accounts
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;
-- 步驟 2: 檢查操作是否成功(有沒有改到資料)
IF NOT FOUND THEN
ROLLBACK; -- 錢不夠就撤銷
RAISE EXCEPTION '餘額不足!'; -- 錯誤!丟出例外
END IF;
-- 步驟 3: 把錢加到第二個帳戶
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
-- 提交 transaction
COMMIT;
這樣,如果客戶想轉的錢比帳戶裡多,transaction 會被撤銷,資料庫就不會出現「卡住」的狀態。
注意事項和常見錯誤
忘記 COMMIT: 如果 transaction 結束時忘了 COMMIT,資料庫會一直「等著」,變更也不會被保存。
忘記 WHERE: 更新或刪除資料時沒加條件,後果可能很慘烈。比如 DELETE FROM students 沒有 WHERE,會把所有學生都刪掉。
transaction 開太久: 如果 transaction 開太久,會鎖住資料,造成效能問題。記得 transaction 要盡快結束(COMMIT 或 ROLLBACK)。
transaction 就是你確保資料完整性的好朋友。它們能幫你避免資料不一致,特別是在像註冊用戶、處理付款或更新關聯表這種複雜情境。學會用 BEGIN、COMMIT、ROLLBACK 和 SAVEPOINT,你就能寫出更穩、更安全的應用程式啦!
GO TO FULL VERSION