開發 trigger 時常見錯誤分析
好啦各位同學,你們現在已經知道 trigger 是什麼、有哪幾種、怎麼運作,甚至已經會寫 trigger 來做各種事。但寫 code 跟 debug 一樣重要,知道「不能怎麼做」有時比「能怎麼做」還重要。今天我們就來聊聊大家在寫 trigger 時最容易踩到的雷,讓你少花幾個小時,甚至幾天在 debug 上面。
Trigger 遞迴:trigger 自己叫自己
這大概是新手最常犯的錯。想像一下,你寫了一個 trigger,會去更新某個欄位,比如 last_modified。但只要這個欄位一被改,update 動作又會再觸發 trigger。這樣就會無限循環,最後 server 直接爆掉,stack overflow error。
範例:
CREATE OR REPLACE FUNCTION update_last_modified()
RETURNS TRIGGER AS $$
BEGIN
-- 更新 last_modified 欄位
UPDATE my_table
SET last_modified = NOW()
WHERE id = NEW.id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER after_update
AFTER UPDATE ON my_table
FOR EACH ROW
EXECUTE FUNCTION update_last_modified();
這裡哪裡怪怪的?function 裡的 UPDATE 會再觸發同一個 trigger,然後就無限 loop 了。
怎麼避免:
用 OLD 變數,先比對值有沒有變再決定要不要動手:
CREATE OR REPLACE FUNCTION update_last_modified_safe()
RETURNS TRIGGER AS $$
BEGIN
-- 檢查值有沒有變
IF NEW.last_modified IS DISTINCT FROM OLD.last_modified THEN
NEW.last_modified = NOW();
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
記得不要在 trigger 裡做多餘的操作。
錯誤使用 OLD 跟 NEW
這兩個變數是 trigger 好朋友,但新手常常搞混。OLD 是改動前的資料,NEW 是改動後要存進去的資料。
常見錯誤是搞錯什麼時候可以用,或是用在不能用的地方。比如 BEFORE INSERT trigger,OLD 根本不存在,因為資料還沒進來。
錯誤範例:
-- 這會出錯,因為 insert 時沒有 OLD
CREATE OR REPLACE FUNCTION log_inserts()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_log (old_data, new_data)
VALUES (OLD.my_column, NEW.my_column);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
怎麼避免:
記得什麼時候該用 OLD、什麼時候該用 NEW:
OLD只有UPDATE跟DELETE用得到。NEW只有INSERT跟UPDATE用得到。
同一操作有多個 trigger
PostgreSQL 允許你在同一個 table、同一個操作上設多個 trigger。看起來很方便,但如果 trigger 互相打架或改到同一份資料,結果就會很亂。
範例:
-- Trigger 1
CREATE OR REPLACE FUNCTION trigger_one()
RETURNS TRIGGER AS $$
BEGIN
-- trigger 1 的邏輯
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger 2
CREATE OR REPLACE FUNCTION trigger_two()
RETURNS TRIGGER AS $$
BEGIN
-- trigger 2 的邏輯
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 建立兩個 trigger
CREATE TRIGGER trigger_one AFTER INSERT ON my_table EXECUTE FUNCTION trigger_one();
CREATE TRIGGER trigger_two AFTER INSERT ON my_table EXECUTE FUNCTION trigger_two();
兩個 trigger 都會在 my_table insert 時被叫到。如果邏輯沒協調好,結果就會很難預期。
怎麼避免:
- 一開始就規劃好 trigger 架構。
- 如果邏輯很像,乾脆合併成一個 trigger。
效能問題
trigger 會讓每次操作多做一些事。如果你在大 table 或高頻率操作上用 trigger,效能可能會掉很多。
錯誤範例:
CREATE OR REPLACE FUNCTION heavy_trigger_function()
RETURNS TRIGGER AS $$
BEGIN
-- 每次更新都做很重的事
PERFORM some_heavy_query();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER performance_killer AFTER UPDATE ON huge_table EXECUTE FUNCTION heavy_trigger_function();
怎麼避免:
- trigger 裡的邏輯越簡單越好。真的要做重的事,考慮丟到 background job。
- 加
WHEN條件,限制 trigger 什麼時候才執行:
CREATE TRIGGER optimized_trigger
AFTER UPDATE ON my_table
WHEN (OLD.column_name IS DISTINCT FROM NEW.column_name)
EXECUTE FUNCTION light_function();
Trigger 跟 transaction
trigger 會在你 query 開的 transaction 裡執行。如果 trigger 出錯,整個 transaction 都會 rollback。有時這很有用,但如果沒想清楚怎麼處理錯誤,可能會出大事。
錯誤範例:
CREATE OR REPLACE FUNCTION error_prone_trigger()
RETURNS TRIGGER AS $$
BEGIN
-- 故意丟錯
RAISE EXCEPTION '有東西壞掉啦!';
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
這個 trigger 只要被叫到,你的主 query 就會被 rollback。
怎麼避免:
在 trigger 裡加錯誤處理,減少對主 transaction 的影響:
CREATE OR REPLACE FUNCTION safe_trigger()
RETURNS TRIGGER AS $$
BEGIN
BEGIN
-- 可能會出錯的 code
INSERT INTO another_table VALUES (NEW.data);
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '有錯誤發生,不過我們有處理好。';
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
實用建議
trigger 越簡單越好。 如果你覺得 trigger 太大太複雜,應該拆成幾個 function 或重想邏輯。
先在小資料量測試 trigger。 不要一開始就丟到重要 table,先在測試環境玩玩看。
記得寫 trigger 文件。 幾個月後你或同事一定會忘記這 trigger 幹嘛的。寫清楚省很多麻煩。
能在 application 層解決的事就不要用 trigger。 trigger 很適合自動化即時處理,但複雜的 business logic 用 trigger 以後會很難維護。
注意效能。 隨時監控 trigger 對資料庫效能的影響,尤其是資料量或流量變大時。
有了這些 tips 跟今天學到的東西,你不只會寫 trigger,還能寫出又正確又有效率、不會出奇怪 bug 的 trigger!
GO TO FULL VERSION