Functions และ triggers
จนถึงตอนนี้ตรรกะทุกชิ้นอยู่ในแอปพลิเคชันของคุณ PostgreSQL ก็รันตรรกะเองได้เช่นกัน คือ function เป็นบล็อกของโค้ดที่มีชื่อและนำกลับมาใช้ซ้ำได้ซึ่งถูกเก็บไว้ในฐานข้อมูล และ trigger เป็นกฎที่สั่งให้ function ทำงานอัตโนมัติทุกครั้งที่ row เปลี่ยนแปลง พอรวมสองอย่างเข้าด้วยกัน คุณก็บังคับใช้พฤติกรรมจากที่เดียวได้ ไม่ว่าแอปพลิเคชันหรือคนใดจะมาแตะ table นั้น
เราทำงานกับ table products ที่ติดตามว่าแต่ละ row ถูกเปลี่ยนครั้งล่าสุดเมื่อใด
CREATE TABLE products ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, price numeric(10, 2) NOT NULL, updated_at timestamptz NOT NULL DEFAULT now());PL/pgSQL function เล็ก ๆ
หัวข้อที่มีชื่อว่า “PL/pgSQL function เล็ก ๆ”CREATE FUNCTION กับ LANGUAGE plpgsql นิยาม function ในภาษาเชิงกระบวนการ (procedural language) ของ PostgreSQL PL/pgSQL เพิ่มตัวแปร เงื่อนไข และลูปบนตัว SQL นี่คือ function เล็ก ๆ ที่ทำงานเดียวและคืนค่า คือใช้ส่วนลดเป็นเปอร์เซ็นต์กับราคา
CREATE FUNCTION apply_discount(price numeric, percent numeric)RETURNS numericLANGUAGE plpgsqlAS $$BEGIN IF percent < 0 OR percent > 100 THEN RAISE EXCEPTION 'percent must be between 0 and 100, got %', percent; END IF; RETURN round(price - (price * percent / 100), 2);END;$$;CREATE FUNCTIONตัว $$ คือ dollar quoting ที่คั่นเนื้อหา (body) ของ function เพื่อให้คุณไม่ต้อง escape เครื่องหมายคำพูดข้างใน เรียก function เหมือนฟังก์ชันในตัวใด ๆ:
SELECT name, price, apply_discount(price, 15) AS sale_priceFROM products; name | price | sale_price-------------+--------+------------ Trail Runner| 120.00 | 102.00 City Loafer | 95.00 | 80.75(2 rows)trigger function
หัวข้อที่มีชื่อว่า “trigger function”trigger ต้องการ function ที่คืนชนิดพิเศษ trigger ภายใน function นั้น record โดยปริยายชื่อ NEW จะถือ row ที่กำลัง insert หรือ update อยู่ ส่วน OLD ถือเวอร์ชันก่อนหน้าเมื่อเป็นการ update ตัวอย่างนี้ประทับเวลาปัจจุบันลงบน updated_at ก่อนที่ row จะถูกเขียนจริง
CREATE FUNCTION set_updated_at()RETURNS triggerLANGUAGE plpgsqlAS $$BEGIN NEW.updated_at := now(); RETURN NEW;END;$$;CREATE FUNCTIONtrigger แบบ BEFORE ต้อง RETURN NEW — row ที่ function คืนกลับมาคือ row ที่ถูกเก็บจริง ดังนั้นนี่คือวิธีที่ trigger แก้ไขข้อมูลขาเข้าได้
การต่อ trigger
หัวข้อที่มีชื่อว่า “การต่อ trigger”CREATE TRIGGER เชื่อม function เข้ากับ table และเหตุการณ์ ตัวนี้จุดชนวน BEFORE UPDATE สำหรับแต่ละ row ที่ได้รับผลกระทบ
CREATE TRIGGER trg_set_updated_atBEFORE UPDATE ON productsFOR EACH ROWEXECUTE FUNCTION set_updated_at();CREATE TRIGGERตอนนี้การ update ใด ๆ จะรีเฟรช timestamp โดยที่แอปพลิเคชันไม่ต้องคอยจำ:
UPDATE products SET price = 110.00 WHERE name = 'Trail Runner'RETURNING name, price, updated_at; name | price | updated_at-------------+--------+------------------------------- Trail Runner| 110.00 | 2026-06-25 09:14:02.118+00(1 row)การ audit ตอน insert
หัวข้อที่มีชื่อว่า “การ audit ตอน insert”แพตเทิร์นเดียวกันใช้ audit การเปลี่ยนแปลงได้ สร้าง table audit, trigger function ที่บันทึก row ใหม่ และ trigger แบบ AFTER INSERT เพื่อให้ log ถูกเขียนเมื่อการ insert สำเร็จ
CREATE TABLE product_audit ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, product_id bigint NOT NULL, action text NOT NULL, logged_at timestamptz NOT NULL DEFAULT now());
CREATE FUNCTION audit_product_insert()RETURNS triggerLANGUAGE plpgsqlAS $$BEGIN INSERT INTO product_audit (product_id, action) VALUES (NEW.id, 'INSERT'); RETURN NEW;END;$$;
CREATE TRIGGER trg_audit_product_insertAFTER INSERT ON productsFOR EACH ROWEXECUTE FUNCTION audit_product_insert();CREATE TRIGGERtrigger แบบ AFTER ทำงานเมื่อ row ถูกเขียนอย่างปลอดภัยแล้ว ซึ่งเหมาะกับการ logging — คุณต้องการรายการ audit เฉพาะสำหรับการ insert ที่เกิดขึ้นจริงเท่านั้น
trigger จุดชนวนอย่างไร
หัวข้อที่มีชื่อว่า “trigger จุดชนวนอย่างไร”ลำดับเป็นแบบเดียวกันเสมอ: คำสั่งเขียนมาถึง PostgreSQL ตรวจพบว่าตรงกับเหตุการณ์ของ trigger, trigger function จึงทำงาน และ operation เดินหน้าต่อด้วยสิ่งที่ function คืนกลับมา
flowchart TD
W[INSERT or UPDATE on products] --> E{Matches a trigger event}
E -->|BEFORE| B[set_updated_at runs, returns NEW]
B --> S[(Row written with new timestamp)]
E -->|AFTER| A[audit_product_insert runs]
S --> A
A --> L[(Row recorded in product_audit)] เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- trigger ซ่อนตรรกะจากใครก็ตามที่อ่านโค้ดแอปพลิเคชัน จงบันทึกเอกสารของทุก trigger — ว่าทำอะไรและอยู่บน table ไหน — เพื่อให้ timestamp ที่อัปเดต “อย่างมหัศจรรย์” ไม่เป็นปริศนา
- ทำให้ function เล็กและมีหน้าที่เดียว function ที่ตรวจสอบ input นั้นทดสอบและนำกลับมาใช้ซ้ำได้ง่าย ส่วน function ที่ทำห้าอย่างไม่เกี่ยวกันจะกลายเป็นความยุ่งเหยิงแบบเดียวกับที่คุณพยายามหลีกเลี่ยง
- เลือก
BEFOREเพื่อแก้ไขหรือตรวจสอบ row ขาเข้า (คุณเปลี่ยนNEWได้) และAFTERสำหรับผลข้างเคียงอย่างการ logging ที่ควรเกิดขึ้นเฉพาะเมื่อการเขียนสำเร็จ - trigger function แบบ row-level ของ
BEFOREต้องRETURN NEWเพื่อปล่อยให้ row ผ่าน การคืนNULLจะยกเลิก operation สำหรับ row นั้นอย่างเงียบ ๆ ซึ่งบางครั้งมีประโยชน์แต่ก็ชวนประหลาดใจ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| function/trigger ในฐานข้อมูล | บังคับใช้เสมอไม่ว่า client หรือ script ไหนจะเขียน table | อ่าน ทดสอบ และ debug ยากกว่าโค้ดแอปพลิเคชัน มองไม่เห็นจาก codebase ของแอป |
| logic ในแอปพลิเคชัน | อ่านง่าย เขียน test ได้ตรงไปตรงมา ใช้ version control และ tooling ปกติ | ไม่ถูกบังคับใช้ถ้ามี client หรือ script อื่นเขียนข้อมูลตรง ๆ โดยข้ามแอปนั้นไป |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ใส่ business logic ที่ซับซ้อนไว้ใน trigger — logic ที่ซับซ้อนควรอยู่ในแอปพลิเคชันที่คนอ่าน codebase มองเห็นได้ ไม่ใช่ซ่อนอยู่ใน trigger ที่ไม่มีใครนึกถึง
- ลืมว่ามี trigger อยู่ — แล้วงงว่าทำไม row เปลี่ยนแปลงโดยที่ไม่มีโค้ดส่วนไหนในแอปสั่งให้เปลี่ยน จงบันทึกเอกสารของทุก trigger ไว้ให้ชัดเจน
- เขียน trigger ที่ทำงานช้า — ทุก insert/update บน table นั้นจะช้าลงตามไปด้วยอย่างเงียบ ๆ เพราะ trigger รันแทรกอยู่ในทุก write ไม่ใช่แค่ query ที่เรียกใช้ตรง ๆ
💡 ตัวอย่างจากของจริง
Audit logging — เป็นเหตุผลคลาสสิกที่ทีมใช้ trigger เพราะต้องบันทึกทุกการเปลี่ยนแปลงไม่ว่า client หรือ script ไหนจะเขียน table นั้น ถ้าฝากไว้ที่โค้ดแอปพลิเคชันอย่างเดียว การเขียนที่ข้ามแอปไปจะไม่ถูกบันทึก
การรักษา
updated_at— หลายทีมใช้ trigger แบบset_updated_atเพื่อรับประกันว่า timestamp ถูกต้องเสมอ แต่ business logic จริง ๆ ของแอป ส่วนใหญ่ทีมจะจงใจเก็บไว้ในโค้ดแอปพลิเคชันแทน เพื่อให้มองเห็นและทดสอบได้ง่าย