Triggers و Rules در PostgreSQL
Triggers به ما اجازه میدهند کدی را قبل یا بعد از INSERT/UPDATE/DELETE روی یک جدول اجرا کنیم. Rules مکانیزمی قدرتمندتر اما پیچیدهتر هستند. در این فصل با BEFORE/AFTER triggers، Statement vs Row level، Constraint Triggers و کاربردهای واقعی آشنا میشویم.
Triggers به ما اجازه میدهند کدی را قبل یا بعد از INSERT/UPDATE/DELETE روی یک جدول اجرا کنیم. Rules مکانیزمی قدرتمندتر اما پیچیدهتر هستند. در این فصل با BEFORE/AFTER triggers، Statement vs Row level، Constraint Triggers و کاربردهای واقعی آشنا میشویم.
Trigger چیست؟
Trigger یک تابع است که خودکار در زمان مشخصی روی یک جدول اجرا میشود:
- قبل (BEFORE) یا بعد (AFTER) از INSERT/UPDATE/DELETE/TRUNCATE
- برای هر ردیف (FOR EACH ROW) یا برای کل statement (FOR EACH STATEMENT)
Trigger اساسی
۱. تابع trigger
-- تابع باید RETURNS TRIGGER باشد
CREATE OR REPLACE FUNCTION update_modified_time()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$;
۲. اتصال trigger به جدول
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
price NUMERIC,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TRIGGER products_update_modified
BEFORE UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION update_modified_time();
-- تست
UPDATE products SET price = 100 WHERE id = 1;
-- updated_at خودکار بهروز میشود
متغیرهای NEW و OLD
در trigger functions:
NEW: ردیف جدید (در INSERT/UPDATE)OLD: ردیف قدیم (در UPDATE/DELETE)TG_OP: نوع operation (INSERT/UPDATE/DELETE)TG_TABLE_NAME: نام جدولTG_WHEN: BEFORE یا AFTER
| Operation | NEW | OLD |
|---|---|---|
| INSERT | ردیف جدید | NULL |
| UPDATE | ردیف جدید | ردیف قدیم |
| DELETE | NULL | ردیف حذفشده |
CREATE OR REPLACE FUNCTION audit_changes()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO audit_log(table_name, action, new_data)
VALUES (TG_TABLE_NAME, 'INSERT', row_to_json(NEW));
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO audit_log(table_name, action, old_data, new_data)
VALUES (TG_TABLE_NAME, 'UPDATE', row_to_json(OLD), row_to_json(NEW));
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
INSERT INTO audit_log(table_name, action, old_data)
VALUES (TG_TABLE_NAME, 'DELETE', row_to_json(OLD));
RETURN OLD;
END IF;
RETURN NULL;
END;
$$;
BEFORE vs AFTER
BEFORE Trigger
قبل از اعمال تغییر اجرا میشود. میتواند:
- NEW را تغییر دهد
- NULL برگرداند → عملیات لغو میشود
- اعتبارسنجی
-- اعتبارسنجی قبل از insert
CREATE OR REPLACE FUNCTION validate_product()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.price <= 0 THEN
RAISE EXCEPTION 'قیمت باید مثبت باشد';
END IF;
-- normalize
NEW.name := TRIM(NEW.name);
NEW.name := REGEXP_REPLACE(NEW.name, 's+', ' ', 'g');
RETURN NEW;
END;
$$;
CREATE TRIGGER products_validate
BEFORE INSERT OR UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION validate_product();
AFTER Trigger
بعد از اعمال تغییر اجرا میشود. مناسب برای:
- Logging و audit
- بهروزرسانی جداول دیگر
- ارسال notification
-- بروزرسانی موجودی بعد از سفارش
CREATE OR REPLACE FUNCTION decrease_stock()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE products
SET stock = stock - NEW.quantity
WHERE id = NEW.product_id;
IF (SELECT stock FROM products WHERE id = NEW.product_id) < 0 THEN
RAISE EXCEPTION 'موجودی کافی نیست';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER order_items_decrease_stock
AFTER INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION decrease_stock();
Conditional Triggers – WHEN
اجرای trigger فقط در شرایط خاص:
-- فقط وقتی status تغییر میکند
CREATE TRIGGER orders_status_change
AFTER UPDATE ON orders
FOR EACH ROW
WHEN (OLD.status IS DISTINCT FROM NEW.status)
EXECUTE FUNCTION log_status_change();
-- فقط برای کاربران VIP
CREATE TRIGGER vip_user_action
AFTER INSERT ON activity_log
FOR EACH ROW
WHEN (NEW.user_type = 'vip')
EXECUTE FUNCTION notify_vip_team();
Row vs Statement Level
FOR EACH ROW (پیشفرض)
-- یکبار به ازای هر ردیف اجرا میشود
CREATE TRIGGER row_trigger
AFTER INSERT ON products
FOR EACH ROW
EXECUTE FUNCTION my_func();
-- INSERT 1000 ردیف → 1000 بار اجرا
FOR EACH STATEMENT
-- یکبار برای کل دستور اجرا میشود
-- NEW و OLD در دسترس نیستند، اما میتوان از Transition Tables استفاده کرد
CREATE OR REPLACE FUNCTION log_bulk_changes()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO bulk_audit_log(table_name, operation, count, executed_at)
SELECT TG_TABLE_NAME, TG_OP, COUNT(*), NOW()
FROM new_table; -- transition table
RETURN NULL;
END;
$$;
CREATE TRIGGER products_bulk_audit
AFTER INSERT ON products
REFERENCING NEW TABLE AS new_table
FOR EACH STATEMENT
EXECUTE FUNCTION log_bulk_changes();
-- transition tables برای DELETE: REFERENCING OLD TABLE AS old_table
-- برای UPDATE: REFERENCING NEW TABLE AS new_t OLD TABLE AS old_t
مثال ۱: updated_at خودکار
-- تابع reusable
CREATE OR REPLACE FUNCTION trigger_set_updated_at()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$;
-- اعمال روی همه جدولها
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON products
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
مثال ۲: Audit Log کامل
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
table_name TEXT,
record_id INTEGER,
operation TEXT,
old_data JSONB,
new_data JSONB,
changed_by TEXT DEFAULT current_user,
changed_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_old JSONB;
v_new JSONB;
v_id INTEGER;
BEGIN
IF TG_OP = 'DELETE' THEN
v_old := to_jsonb(OLD);
v_id := OLD.id;
ELSIF TG_OP = 'UPDATE' THEN
v_old := to_jsonb(OLD);
v_new := to_jsonb(NEW);
v_id := NEW.id;
ELSIF TG_OP = 'INSERT' THEN
v_new := to_jsonb(NEW);
v_id := NEW.id;
END IF;
INSERT INTO audit_log(table_name, record_id, operation, old_data, new_data)
VALUES (TG_TABLE_NAME, v_id, TG_OP, v_old, v_new);
RETURN COALESCE(NEW, OLD);
END;
$$;
CREATE TRIGGER products_audit
AFTER INSERT OR UPDATE OR DELETE ON products
FOR EACH ROW EXECUTE FUNCTION audit_trigger();
CREATE TRIGGER orders_audit
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION audit_trigger();
-- query تغییرات
SELECT * FROM audit_log
WHERE table_name = 'products' AND record_id = 5
ORDER BY changed_at DESC;
مثال ۳: Soft Delete
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
deleted_at TIMESTAMPTZ
);
-- بهجای حذف واقعی، فقط deleted_at تنظیم شود
CREATE OR REPLACE FUNCTION soft_delete()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE products SET deleted_at = NOW() WHERE id = OLD.id;
RETURN NULL; -- لغو DELETE واقعی
END;
$$;
CREATE TRIGGER products_soft_delete
BEFORE DELETE ON products
FOR EACH ROW
WHEN (OLD.deleted_at IS NULL) -- اگر هنوز حذف نشده
EXECUTE FUNCTION soft_delete();
-- تست
DELETE FROM products WHERE id = 1; -- در واقع UPDATE میشود
-- View برای کاربران
CREATE VIEW active_products AS
SELECT * FROM products WHERE deleted_at IS NULL;
مثال ۴: Computed Field بهروز نگه داشتن
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER,
quantity INTEGER,
unit_price NUMERIC(10,2),
line_total NUMERIC(12,2)
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
total NUMERIC(12,2) DEFAULT 0
);
-- محاسبه line_total
CREATE OR REPLACE FUNCTION calc_line_total()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.line_total := NEW.quantity * NEW.unit_price;
RETURN NEW;
END;
$$;
CREATE TRIGGER calc_line_total
BEFORE INSERT OR UPDATE ON order_items
FOR EACH ROW EXECUTE FUNCTION calc_line_total();
-- بروزرسانی total سفارش
CREATE OR REPLACE FUNCTION update_order_total()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_order_id INTEGER;
BEGIN
v_order_id := COALESCE(NEW.order_id, OLD.order_id);
UPDATE orders SET total = (
SELECT COALESCE(SUM(line_total), 0)
FROM order_items
WHERE order_id = v_order_id
) WHERE id = v_order_id;
RETURN COALESCE(NEW, OLD);
END;
$$;
CREATE TRIGGER update_order_total_on_change
AFTER INSERT OR UPDATE OR DELETE ON order_items
FOR EACH ROW EXECUTE FUNCTION update_order_total();
GENERATED ALWAYS AS ... STORED یا computed view استفاده کنید.
Constraint Triggers
Constraint Triggers میتوانند DEFERRABLE باشند – یعنی در پایان transaction اجرا شوند:
CREATE CONSTRAINT TRIGGER check_min_balance
AFTER UPDATE ON accounts
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION check_balance();
-- این مفید است وقتی در یک transaction چند تغییر داریم
-- و فقط در پایان میخواهیم چک کنیم
Event Triggers
Event Triggers سطح دیتابیس هستند (نه جدول) و روی DDL کار میکنند:
-- لاگ همه CREATE TABLE
CREATE OR REPLACE FUNCTION log_ddl_changes()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
DECLARE
obj RECORD;
BEGIN
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands()
LOOP
INSERT INTO ddl_log(command_tag, object_type, object_name)
VALUES (obj.command_tag, obj.object_type, obj.object_identity);
END LOOP;
END;
$$;
CREATE EVENT TRIGGER log_ddl
ON ddl_command_end
EXECUTE FUNCTION log_ddl_changes();
Rules – مکانیزم قدیمیتر
Rules از زمان Postgres اولیه باقی ماندهاند. در عمل، Triggerها بهتر هستند، اما برخی Rules هنوز کاربرد دارند:
-- مثال: View قابل آپدیت
CREATE TABLE products_archive (
id INTEGER, name TEXT, archived_at TIMESTAMPTZ
);
CREATE VIEW products_view AS SELECT id, name FROM products;
CREATE RULE products_view_delete AS
ON DELETE TO products_view
DO INSTEAD (
INSERT INTO products_archive(id, name, archived_at)
SELECT id, name, NOW() FROM products WHERE id = OLD.id;
DELETE FROM products WHERE id = OLD.id;
);
مدیریت Triggerها
-- لیست triggerها
SELECT trigger_name, event_object_table, event_manipulation
FROM information_schema.triggers
ORDER BY event_object_table, trigger_name;
-- یا
SELECT * FROM pg_trigger WHERE NOT tgisinternal;
-- غیرفعال کردن موقت
ALTER TABLE products DISABLE TRIGGER products_audit;
-- فعالسازی مجدد
ALTER TABLE products ENABLE TRIGGER products_audit;
-- غیرفعال کردن همه
ALTER TABLE products DISABLE TRIGGER ALL;
-- حذف
DROP TRIGGER products_audit ON products;
نکات Performance
- Trigger هزینه دارد – برای bulk insert سنگین میشود
- Statement-level triggers سریعترند برای bulk
- BEFORE triggers میتوانند جلوی index update را بگیرند
- برای bulk load، میتوانید موقت trigger را disable کنید
- اگر فقط محاسبه میخواهید، Generated Column بهتر است
-- bulk load سریع
ALTER TABLE products DISABLE TRIGGER ALL;
COPY products FROM '/path/to/data.csv' WITH CSV;
ALTER TABLE products ENABLE TRIGGER ALL;
-- و سپس بهصورت دستی triggerها را اجرا کنید (مثلاً audit log)
آنتیپترنها
- Triggerهای زنجیرهای (trigger روی trigger روی trigger) – دیباگ سخت
- Logic بسیار پیچیده در trigger – بهتر به app برود
- Trigger با side-effect خارج دیتابیس (HTTP call) – شکنندگی
- تغییر چند جدول در یک trigger – lock contention
- تکرار trigger با همان منطق – DRY را رعایت کنید
بهترین شیوهها
- منطق ساده و سریع در trigger
- BEFORE برای validation و normalization
- AFTER برای audit و sync
- WHEN clause برای فیلتر
- توابع reusable که در چند trigger استفاده میشوند
- مستندسازی خوب – triggerها مخفی هستند و دیباگ سخت میشود
- monitoring اجرای triggerها
جمعبندی
- Trigger = تابع + اتصال به جدول روی INSERT/UPDATE/DELETE
- BEFORE برای validation و تغییر داده، AFTER برای logging
- NEW و OLD برای دسترسی به داده
- FOR EACH ROW (پیشفرض) یا FOR EACH STATEMENT
- WHEN clause برای trigger conditional
- Constraint Triggers قابل DEFERRABLE
- Event Triggers برای DDL
- Rules محدود و قدیمی – از Trigger استفاده کنید