طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 122 · برمجة قواعد البيانات
الدرس 21 · الوحدة 9: إنشاء واستخدام الدوال

المحفزات: دوال تعمل تلقائيًّا

قواعد تعمل وحدها مع كل تغيير في البيانات، من أي تطبيق جاء. ⏱ نحو 45 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تكتب دالة محفز RETURNS trigger وتربطها بجدول.
  • تستخدم NEW و OLD وتفرّق بين BEFORE و AFTER.
  • تبني محفزات للتدقيق والتطبيع والمنع.
  • تستخدم TG_OP وشرط WHEN.

١المحفز: كود يعمل تلقائيًّا

المحفز Trigger دالة لا يستدعيها أحد؛ تعمل وحدها حين يقع حدث على جدول: إدخال أو تعديل أو حذف. مثال: سجلّ تدقيق لكل تغيير في الأسعار، أيًّا كان من غيّرها:

query.sql
CREATE TABLE price_log (  product_id int,  old_price  numeric,  new_price  numeric,  changed_at timestamptz DEFAULT now()); CREATE FUNCTION log_price() RETURNS triggerLANGUAGE plpgsqlAS $$BEGIN  INSERT INTO price_log (product_id, old_price, new_price)  VALUES (OLD.id, OLD.price, NEW.price);  RETURN NEW;END;$$; CREATE TRIGGER trg_price_logAFTER UPDATE OF price ON productsFOR EACH ROWWHEN (OLD.price IS DISTINCT FROM NEW.price)EXECUTE FUNCTION log_price(); UPDATE products SET price = price * 1.10 WHERE category_id = 2;UPDATE products SET stock = stock + 1 WHERE id = 1;     -- لا يُسجَّل: لم يتغير السعر SELECT product_id, old_price, new_price FROM price_log ORDER BY product_id;
✓ أُنشئ الجدول
✓ أُنشئت الدالة
✓ أُنشئ المحفّز
✓ عُدِّل 3 صفوف
✓ عُدِّل صف واحد
النتيجة · 3 صفوف
product_idold_pricenew_price
47582.5
5140154
6210231

تعديل المخزون لم يُشغّل المحفز، لأنه مربوط بعمود price فقط ومشروط بتغيّر القيمة.

الجزءمعناه
RETURNS triggerدالة المحفز: بلا معاملات، ونوعها trigger
OLD و NEWالصف قبل التغيير وبعده (لا يوجد OLD في INSERT، ولا NEW في DELETE)
AFTER UPDATE OF price ON productsمتى: بعد تعديل عمود السعر
FOR EACH ROWمرة لكل صف متأثر
WHEN (…)شرط اختياري لتشغيله

٢BEFORE: تعديل الصف أو رفضه قبل حفظه

المحفز BEFORE يعمل قبل الحفظ، فيستطيع تعديل NEW، أو رفض العملية بخطأ:

query.sql
CREATE FUNCTION clean_customer() RETURNS triggerLANGUAGE plpgsqlAS $$BEGIN  NEW.email := lower(trim(NEW.email));     -- توحيد البريد قبل الحفظ  NEW.name  := trim(NEW.name);  RETURN NEW;                              -- يُحفظ الصف المعدّلEND;$$; CREATE TRIGGER trg_clean_customerBEFORE INSERT OR UPDATE ON customersFOR EACH ROW EXECUTE FUNCTION clean_customer(); INSERT INTO customers (name, city, email, joined)VALUES ('  لمى ', 'جدة', '  Lama@MAIL.sa ', current_date);SELECT '[' || name || ']' AS الاسم, email FROM customers WHERE city = 'جدة' ORDER BY id DESC LIMIT 1;
✓ أُنشئت الدالة
✓ أُنشئ المحفّز
✓ أُضيف صف واحد
النتيجة · صف واحد
الاسمemail
[لمى]lama@mail.sa
query.sql
CREATE FUNCTION no_delete_paid() RETURNS triggerLANGUAGE plpgsqlAS $$BEGIN  IF OLD.status IN ('paid', 'shipped') THEN    RAISE EXCEPTION 'لا يُحذف طلب % حالته %؛ ألغِه بدل حذفه', OLD.id, OLD.status;  END IF;  RETURN OLD;END;$$; CREATE TRIGGER trg_no_delete_paidBEFORE DELETE ON ordersFOR EACH ROW EXECUTE FUNCTION no_delete_paid(); DELETE FROM orders WHERE id = 2;
✓ أُنشئت الدالة
✓ أُنشئ المحفّز
✗ خطأ (السطر 16): لا يُحذف طلب 2 حالته paid؛ ألغِه بدل حذفه
في الدالة no_delete_paid()، السطر 4 داخلها (RAISE)

٣محفز واحد لعدة أحداث: TG_OP

query.sql
CREATE TABLE stock_events (op text, product_id int, qty int); CREATE FUNCTION track_items() RETURNS triggerLANGUAGE plpgsqlAS $$BEGIN  IF TG_OP = 'INSERT' THEN    INSERT INTO stock_events VALUES ('إضافة بند', NEW.product_id, NEW.qty);  ELSIF TG_OP = 'DELETE' THEN    INSERT INTO stock_events VALUES ('حذف بند', OLD.product_id, OLD.qty);  END IF;  RETURN NULL;          -- في AFTER لا يهم ما يُرجعEND;$$; CREATE TRIGGER trg_track_itemsAFTER INSERT OR DELETE ON order_itemsFOR EACH ROW EXECUTE FUNCTION track_items(); INSERT INTO order_items VALUES (11, 1, 1, 3200);DELETE FROM order_items WHERE order_id = 11 AND product_id = 9;SELECT * FROM stock_events;
✓ أُنشئ الجدول
✓ أُنشئت الدالة
✓ أُنشئ المحفّز
✓ أُضيف صف واحد
✓ حُذف صف واحد
النتيجة · صفّان
opproduct_idqty
إضافة بند11
حذف بند920

توقّع وتحقّق

لتوحيد البريد قبل حفظه نستخدم محفز:

في محفز DELETE، الصف المحذوف في:

TG_OP يحتوي:

في Oracle يُكتب الصف الجديد:

الخلاصة

تحدٍّ برمجي: خصم المخزون تلقائيًّا

متقدم

اكتب محفزًا على order_items ينقص مخزون المنتج تلقائيًّا عند إضافة بند، ويرفض البند إن لم يكفِ المخزون.

المطلوب:
  • دالة reserve_stock() ومحفز BEFORE INSERT … FOR EACH ROW.
  • إن كان المخزون أقل من NEW.qty: RAISE EXCEPTION.
  • وإلا: أنقص products.stock بمقدار NEW.qty وأكمل الإدخال.
👀 اعرض الناتج المطلوب
✓ التحقق الآلي من حلّك:
  • الكود يعمل بلا أخطاء
  • بند 5 فأرات ← المخزون 55 والبند محفوظ
  • بند أكبر من المخزون يُرفض
  • الرفض لم يغيّر المخزون
  • BEFORE INSERT
  • FOR EACH ROW

💡 تلميح 1
ابدأ من الإجراء sell في الدرس 18، وحوّله إلى دالة RETURNS trigger تستخدم NEW.product_id و NEW.qty.
💡 تلميح 2
لا تنسَ RETURN NEW; في آخرها.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

solution.sql
CREATE FUNCTION reserve_stock() RETURNS triggerLANGUAGE plpgsqlAS $$DECLARE  have int;BEGIN  SELECT stock INTO have FROM products WHERE id = NEW.product_id;  IF have < NEW.qty THEN    RAISE EXCEPTION 'المخزون غير كافٍ للمنتج %: المتوفر %', NEW.product_id, have;  END IF;  UPDATE products SET stock = stock - NEW.qty WHERE id = NEW.product_id;  RETURN NEW;END;$$; CREATE TRIGGER trg_reserve_stockBEFORE INSERT ON order_itemsFOR EACH ROW EXECUTE FUNCTION reserve_stock();
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس