المشغلات (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 مع تعديل البيانات
-- مشغل يحول البريد الإلكتروني إلى أحرف صغيرة قبل الحفظ
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 مفيدة جداً لتتبع التغييرات وحفظ سجلات التدقيق.
-- جدول لحفظ تاريخ تغييرات الرواتب
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 لمنع تغييرات معينة
-- مشغل يمنع تخفيض الراتب بأكثر من 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 مفيدة لحفظ نسخة من البيانات المحذوفة أو تنظيف البيانات المرتبطة.
-- جدول لحفظ الموظفين المحذوفين
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 لتنظيف البيانات المرتبطة
-- مشغل يحذف جميع الطلبات عند حذف عميل
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) وتطوير مهاراتك في قواعد البيانات.
الانتقال إلى الدرس التالي