ข้ามไปยังเนื้อหา

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()
);

CREATE FUNCTION กับ LANGUAGE plpgsql นิยาม function ในภาษาเชิงกระบวนการ (procedural language) ของ PostgreSQL PL/pgSQL เพิ่มตัวแปร เงื่อนไข และลูปบนตัว SQL นี่คือ function เล็ก ๆ ที่ทำงานเดียวและคืนค่า คือใช้ส่วนลดเป็นเปอร์เซ็นต์กับราคา

CREATE FUNCTION apply_discount(price numeric, percent numeric)
RETURNS numeric
LANGUAGE plpgsql
AS $$
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_price
FROM products;
name | price | sale_price
-------------+--------+------------
Trail Runner| 120.00 | 102.00
City Loafer | 95.00 | 80.75
(2 rows)

trigger ต้องการ function ที่คืนชนิดพิเศษ trigger ภายใน function นั้น record โดยปริยายชื่อ NEW จะถือ row ที่กำลัง insert หรือ update อยู่ ส่วน OLD ถือเวอร์ชันก่อนหน้าเมื่อเป็นการ update ตัวอย่างนี้ประทับเวลาปัจจุบันลงบน updated_at ก่อนที่ row จะถูกเขียนจริง

CREATE FUNCTION set_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$$;
CREATE FUNCTION

trigger แบบ BEFORE ต้อง RETURN NEW — row ที่ function คืนกลับมาคือ row ที่ถูกเก็บจริง ดังนั้นนี่คือวิธีที่ trigger แก้ไขข้อมูลขาเข้าได้

CREATE TRIGGER เชื่อม function เข้ากับ table และเหตุการณ์ ตัวนี้จุดชนวน BEFORE UPDATE สำหรับแต่ละ row ที่ได้รับผลกระทบ

CREATE TRIGGER trg_set_updated_at
BEFORE UPDATE ON products
FOR EACH ROW
EXECUTE 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 การเปลี่ยนแปลงได้ สร้าง 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 trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO product_audit (product_id, action)
VALUES (NEW.id, 'INSERT');
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_audit_product_insert
AFTER INSERT ON products
FOR EACH ROW
EXECUTE FUNCTION audit_product_insert();
CREATE TRIGGER

trigger แบบ AFTER ทำงานเมื่อ row ถูกเขียนอย่างปลอดภัยแล้ว ซึ่งเหมาะกับการ logging — คุณต้องการรายการ audit เฉพาะสำหรับการ insert ที่เกิดขึ้นจริงเท่านั้น

ลำดับเป็นแบบเดียวกันเสมอ: คำสั่งเขียนมาถึง 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 ที่ตรงกัน ซึ่งรัน function
  • trigger ซ่อนตรรกะจากใครก็ตามที่อ่านโค้ดแอปพลิเคชัน จงบันทึกเอกสารของทุก trigger — ว่าทำอะไรและอยู่บน table ไหน — เพื่อให้ timestamp ที่อัปเดต “อย่างมหัศจรรย์” ไม่เป็นปริศนา
  • ทำให้ function เล็กและมีหน้าที่เดียว function ที่ตรวจสอบ input นั้นทดสอบและนำกลับมาใช้ซ้ำได้ง่าย ส่วน function ที่ทำห้าอย่างไม่เกี่ยวกันจะกลายเป็นความยุ่งเหยิงแบบเดียวกับที่คุณพยายามหลีกเลี่ยง
  • เลือก BEFORE เพื่อแก้ไขหรือตรวจสอบ row ขาเข้า (คุณเปลี่ยน NEW ได้) และ AFTER สำหรับผลข้างเคียงอย่างการ logging ที่ควรเกิดขึ้นเฉพาะเมื่อการเขียนสำเร็จ
  • trigger function แบบ row-level ของ BEFORE ต้อง RETURN NEW เพื่อปล่อยให้ row ผ่าน การคืน NULL จะยกเลิก operation สำหรับ row นั้นอย่างเงียบ ๆ ซึ่งบางครั้งมีประโยชน์แต่ก็ชวนประหลาดใจ
ตัวเลือกBenefitCost
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 จริง ๆ ของแอป ส่วนใหญ่ทีมจะจงใจเก็บไว้ในโค้ดแอปพลิเคชันแทน เพื่อให้มองเห็นและทดสอบได้ง่าย

function ต้องคืนค่าชนิดใดเพื่อให้ trigger ใช้ได้?
ภายใน trigger function แบบ BEFORE record NEW ถืออะไรไว้?
ทำไม trigger แบบ AFTER จึงเป็นทางเลือกที่ถูกต้องสำหรับการเขียน audit log?