~/icsd.ir — bash
SYSTEM_ONLINE

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();
توجه: این الگو مفید است اما در حجم بالا می‌تواند کند شود. برای performance بحرانی، از 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 استفاده کنید. Rules محدودیت‌های زیاد و رفتار غیر intuitive دارند.

مدیریت 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 استفاده کنید

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.