المشغلات (Triggers)

المشغلات (Triggers) في SQL هي إجراءات خاصة تُنفذ تلقائياً عند حدوث حدث معين في قاعدة البيانات، مثل إدراج صف جديد، تحديث بيانات، أو حذف سجل. المشغلات تعمل بشكل تلقائي وشفاف دون الحاجة لاستدعائها يدوياً، مما يجعلها أداة قوية جداً لفرض قواعد العمل، الحفاظ على سلامة البيانات، إنشاء سجلات التدقيق، وأتمتة المهام المعقدة. في هذا الدرس الشامل والمفصل من سلسلة تعلم لغة SQL باللغة العربية، سنتعلم كل شيء عن المشغلات: ما هي، كيف تعمل، أنواعها المختلفة (BEFORE و AFTER)، الأحداث التي تطلقها (INSERT, UPDATE, DELETE)، كيفية استخدام NEW و OLD للوصول للبيانات، وأمثلة عملية تطبيقية مفصلة تغطي سيناريوهات واقعية.

1. ما هي المشغلات (Triggers)؟

المشغل (Trigger) هو كائن في قاعدة البيانات يُنفذ تلقائياً عند حدوث حدث معين على جدول محدد. على عكس الإجراءات المخزنة التي تُستدعى يدوياً باستخدام CALL، المشغلات تُنفذ تلقائياً عندما يحدث الحدث المرتبط بها.

لفهم المشغلات بشكل أفضل، تخيل أنك تدير نظام مخزون. في كل مرة يتم بيع منتج (إدراج صف في جدول المبيعات)، تريد تلقائياً تحديث كمية المخزون في جدول المنتجات. بدلاً من كتابة كود في التطبيق لتحديث المخزون في كل مرة، يمكنك إنشاء مشغل يقوم بذلك تلقائياً. عندما يُدرج صف جديد في جدول المبيعات، يُطلق المشغل تلقائياً ويحدث المخزون.

المشغلات مفيدة جداً في الحالات التالية: حفظ سجلات التدقيق (Audit Logs)، فرض قواعد عمل معقدة، الحفاظ على البيانات المشتقة محدثة، منع عمليات غير مصرح بها، وإرسال إشعارات تلقائية. لكن يجب استخدامها بحذر لأنها قد تؤثر على الأداء إذا كانت معقدة جداً.

2. أنواع المشغلات

المشغلات في SQL تُصنف حسب نوعين رئيسيين: توقيت التنفيذ والحدث المُطلق.

حسب توقيت التنفيذ
  • BEFORE Trigger: يُنفذ قبل حدوث العملية (INSERT, UPDATE, DELETE). يمكن استخدامه للتحقق من البيانات أو تعديلها قبل حفظها.
  • AFTER Trigger: يُنفذ بعد حدوث العملية. يُستخدم عادةً لتحديث جداول أخرى أو حفظ سجلات التدقيق.
حسب الحدث المُطلق
  • INSERT Trigger: يُطلق عند إدراج صف جديد
  • UPDATE Trigger: يُطلق عند تحديث صف موجود
  • DELETE Trigger: يُطلق عند حذف صف

يمكن دمج النوعين معاً، مثل: BEFORE INSERT, AFTER UPDATE, BEFORE DELETE، وهكذا. لكل جدول، يمكن أن يكون لديك مشغل واحد فقط لكل نوع (في MySQL).

3. إنشاء مشغل: CREATE TRIGGER

الصيغة الأساسية
الصيغة الأساسية لإنشاء مشغل
DELIMITER //

CREATE TRIGGER trigger_name
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON table_name
FOR EACH ROW
BEGIN
    -- أوامر SQL هنا
END //

DELIMITER ;
مثال بسيط: AFTER INSERT Trigger
مشغل بسيط لحفظ سجل التدقيق
-- جدول الموظفين
CREATE TABLE employees (
    employee_id INT PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    salary DECIMAL(10, 2),
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- جدول سجل التدقيق
CREATE TABLE employee_audit (
    audit_id INT PRIMARY KEY AUTO_INCREMENT,
    employee_id INT,
    action VARCHAR(50),
    action_date DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- مشغل يُنفذ بعد إدراج موظف جديد
DELIMITER //

CREATE TRIGGER after_employee_insert
AFTER INSERT ON employees
FOR EACH ROW
BEGIN
    INSERT INTO employee_audit (employee_id, action)
    VALUES (NEW.employee_id, 'تم إضافة موظف جديد');
END //

DELIMITER ;

-- اختبار المشغل
INSERT INTO employees (first_name, last_name, salary)
VALUES ('أحمد', 'محمد', 5000);

-- التحقق من سجل التدقيق
SELECT * FROM employee_audit;

4. استخدام NEW و OLD في المشغلات

داخل المشغل، يمكنك الوصول إلى قيم الصف باستخدام الكلمات المفتاحية NEW و OLD:

  • NEW: يحتوي على القيم الجديدة (متاح في INSERT و UPDATE)
  • OLD: يحتوي على القيم القديمة (متاح في UPDATE و DELETE)
نوع المشغل NEW متاح؟ OLD متاح؟
BEFORE INSERT نعم (قابل للتعديل) لا
AFTER INSERT نعم (للقراءة فقط) لا
BEFORE UPDATE نعم (قابل للتعديل) نعم (للقراءة فقط)
AFTER UPDATE نعم (للقراءة فقط) نعم (للقراءة فقط)
BEFORE DELETE لا نعم (للقراءة فقط)
AFTER DELETE لا نعم (للقراءة فقط)
مثال: BEFORE INSERT مع تعديل البيانات
BEFORE INSERT لتعديل البيانات قبل الحفظ
-- مشغل يحول البريد الإلكتروني إلى أحرف صغيرة قبل الحفظ
DELIMITER //

CREATE TRIGGER before_employee_insert
BEFORE INSERT ON employees
FOR EACH ROW
BEGIN
    -- تحويل البريد الإلكتروني إلى أحرف صغيرة
    SET NEW.email = LOWER(NEW.email);
    
    -- التأكد من أن الراتب لا يقل عن الحد الأدنى
    IF NEW.salary < 3000 THEN
        SET NEW.salary = 3000;
    END IF;
END //

DELIMITER ;

-- اختبار
INSERT INTO employees (first_name, last_name, email, salary)
VALUES ('فاطمة', 'أحمد', 'FATIMA@EXAMPLE.COM', 2500);

-- البريد سيُحفظ بأحرف صغيرة والراتب سيكون 3000

5. مشغلات UPDATE

مشغلات UPDATE مفيدة جداً لتتبع التغييرات وحفظ سجلات التدقيق.

AFTER UPDATE لحفظ سجل التغييرات
-- جدول لحفظ تاريخ تغييرات الرواتب
CREATE TABLE salary_history (
    history_id INT PRIMARY KEY AUTO_INCREMENT,
    employee_id INT,
    old_salary DECIMAL(10, 2),
    new_salary DECIMAL(10, 2),
    changed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    changed_by VARCHAR(100)
);

-- مشغل يحفظ تاريخ تغييرات الراتب
DELIMITER //

CREATE TRIGGER after_salary_update
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
    -- فقط إذا تغير الراتب
    IF OLD.salary != NEW.salary THEN
        INSERT INTO salary_history (employee_id, old_salary, new_salary, changed_by)
        VALUES (NEW.employee_id, OLD.salary, NEW.salary, USER());
    END IF;
END //

DELIMITER ;

-- اختبار
UPDATE employees SET salary = 6000 WHERE employee_id = 1;

-- التحقق من السجل
SELECT * FROM salary_history;
BEFORE UPDATE لمنع تغييرات معينة
BEFORE UPDATE لفرض قواعد العمل
-- مشغل يمنع تخفيض الراتب بأكثر من 10%
DELIMITER //

CREATE TRIGGER before_salary_decrease
BEFORE UPDATE ON employees
FOR EACH ROW
BEGIN
    IF NEW.salary < OLD.salary THEN
        -- حساب نسبة التخفيض
        IF (OLD.salary - NEW.salary) / OLD.salary > 0.10 THEN
            -- إلغاء التحديث برفع خطأ
            SIGNAL SQLSTATE '45000'
            SET MESSAGE_TEXT = 'لا يمكن تخفيض الراتب بأكثر من 10%';
        END IF;
    END IF;
END //

DELIMITER ;

-- هذا سيفشل
UPDATE employees SET salary = 2000 WHERE employee_id = 1 AND salary = 5000;

6. مشغلات DELETE

مشغلات DELETE مفيدة لحفظ نسخة من البيانات المحذوفة أو تنظيف البيانات المرتبطة.

BEFORE DELETE لحفظ نسخة احتياطية
-- جدول لحفظ الموظفين المحذوفين
CREATE TABLE deleted_employees (
    deleted_id INT PRIMARY KEY AUTO_INCREMENT,
    employee_id INT,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    email VARCHAR(100),
    salary DECIMAL(10, 2),
    deleted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    deleted_by VARCHAR(100)
);

-- مشغل يحفظ نسخة من الموظف قبل الحذف
DELIMITER //

CREATE TRIGGER before_employee_delete
BEFORE DELETE ON employees
FOR EACH ROW
BEGIN
    INSERT INTO deleted_employees (
        employee_id, 
        first_name, 
        last_name, 
        email, 
        salary,
        deleted_by
    )
    VALUES (
        OLD.employee_id,
        OLD.first_name,
        OLD.last_name,
        OLD.email,
        OLD.salary,
        USER()
    );
END //

DELIMITER ;

-- اختبار
DELETE FROM employees WHERE employee_id = 1;

-- التحقق من النسخة المحفوظة
SELECT * FROM deleted_employees;
AFTER DELETE لتنظيف البيانات المرتبطة
AFTER DELETE لحذف البيانات المرتبطة
-- مشغل يحذف جميع الطلبات عند حذف عميل
DELIMITER //

CREATE TRIGGER after_customer_delete
AFTER DELETE ON customers
FOR EACH ROW
BEGIN
    -- حذف جميع طلبات العميل
    DELETE FROM orders WHERE customer_id = OLD.customer_id;
    
    -- حفظ سجل في جدول التدقيق
    INSERT INTO audit_log (action, description)
    VALUES ('DELETE_CUSTOMER', CONCAT('تم حذف العميل: ', OLD.customer_name));
END //

DELIMITER ;

7. مثال عملي شامل: نظام مخزون تلقائي

لنطبق ما تعلمناه في مثال واقعي متكامل: نظام يحدث المخزون تلقائياً عند البيع.

الجداول الأساسية
-- جدول المنتجات
CREATE TABLE products (
    product_id INT PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(100),
    stock_quantity INT DEFAULT 0,
    price DECIMAL(10, 2)
);

-- جدول المبيعات
CREATE TABLE sales (
    sale_id INT PRIMARY KEY AUTO_INCREMENT,
    product_id INT,
    quantity_sold INT,
    sale_date DATETIME DEFAULT CURRENT_TIMESTAMP,
    total_amount DECIMAL(10, 2),
    FOREIGN KEY (product_id) REFERENCES products(product_id)
);

-- جدول سجل المخزون
CREATE TABLE inventory_log (
    log_id INT PRIMARY KEY AUTO_INCREMENT,
    product_id INT,
    change_type VARCHAR(20),
    quantity_change INT,
    old_quantity INT,
    new_quantity INT,
    log_date DATETIME DEFAULT CURRENT_TIMESTAMP
);
المشغلات للنظام
مشغلات نظام المخزون
-- 1. مشغل BEFORE INSERT للتحقق من توفر المخزون
DELIMITER //

CREATE TRIGGER before_sale_insert
BEFORE INSERT ON sales
FOR EACH ROW
BEGIN
    DECLARE available_stock INT;
    DECLARE product_price DECIMAL(10, 2);
    
    -- الحصول على الكمية المتاحة والسعر
    SELECT stock_quantity, price 
    INTO available_stock, product_price
    FROM products
    WHERE product_id = NEW.product_id;
    
    -- التحقق من توفر الكمية
    IF available_stock < NEW.quantity_sold THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'الكمية المطلوبة غير متوفرة في المخزون';
    END IF;
    
    -- حساب المبلغ الإجمالي تلقائياً
    SET NEW.total_amount = NEW.quantity_sold * product_price;
END //

DELIMITER ;

-- 2. مشغل AFTER INSERT لتحديث المخزون
DELIMITER //

CREATE TRIGGER after_sale_insert
AFTER INSERT ON sales
FOR EACH ROW
BEGIN
    DECLARE old_stock INT;
    DECLARE new_stock INT;
    
    -- الحصول على الكمية القديمة
    SELECT stock_quantity INTO old_stock
    FROM products
    WHERE product_id = NEW.product_id;
    
    -- تحديث المخزون
    UPDATE products
    SET stock_quantity = stock_quantity - NEW.quantity_sold
    WHERE product_id = NEW.product_id;
    
    -- حساب الكمية الجديدة
    SET new_stock = old_stock - NEW.quantity_sold;
    
    -- حفظ سجل التغيير
    INSERT INTO inventory_log (
        product_id, 
        change_type, 
        quantity_change,
        old_quantity,
        new_quantity
    )
    VALUES (
        NEW.product_id,
        'SALE',
        -NEW.quantity_sold,
        old_stock,
        new_stock
    );
END //

DELIMITER ;

-- 3. مشغل لإلغاء البيع (عند الحذف)
DELIMITER //

CREATE TRIGGER after_sale_delete
AFTER DELETE ON sales
FOR EACH ROW
BEGIN
    DECLARE old_stock INT;
    
    SELECT stock_quantity INTO old_stock
    FROM products
    WHERE product_id = OLD.product_id;
    
    -- إرجاع الكمية للمخزون
    UPDATE products
    SET stock_quantity = stock_quantity + OLD.quantity_sold
    WHERE product_id = OLD.product_id;
    
    -- حفظ سجل الإرجاع
    INSERT INTO inventory_log (
        product_id,
        change_type,
        quantity_change,
        old_quantity,
        new_quantity
    )
    VALUES (
        OLD.product_id,
        'RETURN',
        OLD.quantity_sold,
        old_stock,
        old_stock + OLD.quantity_sold
    );
END //

DELIMITER ;
اختبار النظام
اختبار المشغلات
-- إضافة منتج
INSERT INTO products (product_name, stock_quantity, price)
VALUES ('لابتوب', 50, 3000);

-- بيع 5 قطع (سيحدث المخزون تلقائياً)
INSERT INTO sales (product_id, quantity_sold)
VALUES (1, 5);

-- التحقق من المخزون (يجب أن يكون 45)
SELECT * FROM products WHERE product_id = 1;

-- التحقق من سجل المخزون
SELECT * FROM inventory_log;

-- محاولة بيع كمية غير متوفرة (سيفشل)
INSERT INTO sales (product_id, quantity_sold)
VALUES (1, 100);

8. مثال: نظام تدقيق شامل

مشغلات لحفظ سجل كامل لجميع التغييرات على جدول معين.

نظام تدقيق شامل
-- جدول التدقيق الشامل
CREATE TABLE audit_trail (
    audit_id INT PRIMARY KEY AUTO_INCREMENT,
    table_name VARCHAR(50),
    operation VARCHAR(10),
    record_id INT,
    old_values TEXT,
    new_values TEXT,
    changed_by VARCHAR(100),
    changed_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- مشغل INSERT
DELIMITER //

CREATE TRIGGER audit_employee_insert
AFTER INSERT ON employees
FOR EACH ROW
BEGIN
    INSERT INTO audit_trail (table_name, operation, record_id, new_values, changed_by)
    VALUES (
        'employees',
        'INSERT',
        NEW.employee_id,
        CONCAT('Name: ', NEW.first_name, ' ', NEW.last_name, ', Salary: ', NEW.salary),
        USER()
    );
END //

DELIMITER ;

-- مشغل UPDATE
DELIMITER //

CREATE TRIGGER audit_employee_update
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
    INSERT INTO audit_trail (
        table_name, 
        operation, 
        record_id, 
        old_values, 
        new_values, 
        changed_by
    )
    VALUES (
        'employees',
        'UPDATE',
        NEW.employee_id,
        CONCAT('Name: ', OLD.first_name, ' ', OLD.last_name, ', Salary: ', OLD.salary),
        CONCAT('Name: ', NEW.first_name, ' ', NEW.last_name, ', Salary: ', NEW.salary),
        USER()
    );
END //

DELIMITER ;

-- مشغل DELETE
DELIMITER //

CREATE TRIGGER audit_employee_delete
BEFORE DELETE ON employees
FOR EACH ROW
BEGIN
    INSERT INTO audit_trail (table_name, operation, record_id, old_values, changed_by)
    VALUES (
        'employees',
        'DELETE',
        OLD.employee_id,
        CONCAT('Name: ', OLD.first_name, ' ', OLD.last_name, ', Salary: ', OLD.salary),
        USER()
    );
END //

DELIMITER ;

9. عرض وحذف المشغلات

عرض المشغلات الموجودة
عرض جميع المشغلات
-- عرض جميع المشغلات في قاعدة البيانات
SHOW TRIGGERS;

-- عرض مشغلات جدول معين
SHOW TRIGGERS WHERE `Table` = 'employees';

-- عرض تعريف مشغل معين
SHOW CREATE TRIGGER after_employee_insert;

-- من جداول النظام
SELECT 
    TRIGGER_NAME,
    EVENT_MANIPULATION,
    EVENT_OBJECT_TABLE,
    ACTION_TIMING
FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = DATABASE();
حذف مشغل
حذف مشغل
-- حذف مشغل
DROP TRIGGER after_employee_insert;

-- حذف آمن
DROP TRIGGER IF EXISTS after_employee_insert;

10. أفضل الممارسات والتحذيرات

أفضل الممارسات
  • استخدم المشغلات للمهام البسيطة والسريعة فقط
  • تجنب المنطق المعقد في المشغلات
  • استخدم أسماء واضحة ومعبرة للمشغلات
  • وثق المشغلات بتعليقات توضيحية
  • اختبر المشغلات بشكل شامل قبل النشر
  • استخدم سجلات التدقيق لتتبع التغييرات
  • تجنب إنشاء مشغلات متداخلة (trigger يطلق trigger آخر)
التحذيرات المهمة
  • المشغلات تؤثر على الأداء - استخدمها بحذر
  • المشغلات تُنفذ لكل صف - قد تكون بطيئة مع العمليات الكبيرة
  • لا يمكن استدعاء المشغلات يدوياً
  • المشغلات قد تخفي منطق العمل عن المطورين
  • صعوبة تتبع الأخطاء في المشغلات
  • تجنب تعديل نفس الجدول في المشغل (قد يسبب حلقة لا نهائية)
  • في MySQL، لا يمكن أن يكون لديك أكثر من مشغل واحد لنفس الحدث والتوقيت
تحذير مهم:

استخدم المشغلات بحكمة. في كثير من الحالات، يكون من الأفضل وضع المنطق في التطبيق أو في إجراءات مخزنة بدلاً من المشغلات، لأن ذلك يجعل الكود أكثر وضوحاً وسهولة في الصيانة.

11. متى تستخدم المشغلات ومتى تتجنبها؟

استخدم المشغلات عندما
  • تحتاج لحفظ سجلات تدقيق تلقائية
  • تريد فرض قواعد عمل على مستوى قاعدة البيانات
  • تحتاج لتحديث بيانات مشتقة تلقائياً
  • تريد الحفاظ على سلامة البيانات بين جداول مرتبطة
  • تحتاج لحفظ نسخة احتياطية من البيانات المحذوفة
تجنب المشغلات عندما
  • المنطق معقد ويحتاج لعدة خطوات
  • تحتاج لمرونة في تنفيذ أو عدم تنفيذ العملية
  • الأداء حرج جداً
  • تحتاج لتتبع وتصحيح الأخطاء بسهولة
  • المنطق يحتاج لتحديثات متكررة
ملخص الدرس

في هذا الدرس الشامل، تعلمنا كل شيء عن المشغلات (Triggers) في SQL:

  • ما هي المشغلات وكيف تعمل تلقائياً
  • أنواع المشغلات: BEFORE و AFTER
  • الأحداث: INSERT, UPDATE, DELETE
  • استخدام NEW و OLD للوصول للبيانات
  • أمثلة عملية: نظام مخزون تلقائي
  • نظام تدقيق شامل
  • أفضل الممارسات والتحذيرات
  • متى تستخدم ومتى تتجنب المشغلات

المشغلات أداة قوية للأتمتة، لكن يجب استخدامها بحذر وحكمة. في كثير من الحالات، الإجراءات المخزنة أو منطق التطبيق قد يكون خياراً أفضل.

الخطوة التالية: المعاملات (Transactions)

أكمل رحلتك التعليمية وانتقل إلى الدرس التالي لتعلم المعاملات (Transactions) وتطوير مهاراتك في قواعد البيانات.

الانتقال إلى الدرس التالي
المحرر الذكي

اكتب الكود وشاهد النتيجة فوراً

جرب الآن مجاناً
قناة ديف عربي

تابع أحدث الدروس والتحديثات مباشرة على واتساب

انضم الآن