الدرس 21 · الوحدة 9: إنشاء واستخدام الدوال
المحفزات: دوال تعمل تلقائيًّا
قواعد تعمل وحدها مع كل تغيير في البيانات، من أي تطبيق جاء. ⏱ نحو 45 دقيقة.
بعد هذا الدرس ستستطيع أن:
- تكتب دالة محفز
RETURNS triggerوتربطها بجدول. - تستخدم
NEWوOLDوتفرّق بينBEFOREوAFTER. - تبني محفزات للتدقيق والتطبيع والمنع.
- تستخدم
TG_OPوشرطWHEN.
١المحفز: كود يعمل تلقائيًّا
المحفز Trigger دالة لا يستدعيها أحد؛ تعمل وحدها حين يقع حدث على جدول: إدخال أو تعديل أو حذف. مثال: سجلّ تدقيق لكل تغيير في الأسعار، أيًّا كان من غيّرها:
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 صفوف
✓ عُدِّل صف واحد
| product_id | old_price | new_price |
|---|---|---|
| 4 | 75 | 82.5 |
| 5 | 140 | 154 |
| 6 | 210 | 231 |
تعديل المخزون لم يُشغّل المحفز، لأنه مربوط بعمود price فقط ومشروط بتغيّر القيمة.
| الجزء | معناه |
|---|---|
RETURNS trigger | دالة المحفز: بلا معاملات، ونوعها trigger |
OLD و NEW | الصف قبل التغيير وبعده (لا يوجد OLD في INSERT، ولا NEW في DELETE) |
AFTER UPDATE OF price ON products | متى: بعد تعديل عمود السعر |
FOR EACH ROW | مرة لكل صف متأثر |
WHEN (…) | شرط اختياري لتشغيله |
٢BEFORE: تعديل الصف أو رفضه قبل حفظه
المحفز BEFORE يعمل قبل الحفظ، فيستطيع تعديل NEW، أو رفض العملية بخطأ:
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;
✓ أُنشئت الدالة
✓ أُنشئ المحفّز
✓ أُضيف صف واحد
| الاسم | |
|---|---|
| [لمى] | lama@mail.sa |
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
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;
✓ أُنشئ الجدول
✓ أُنشئت الدالة
✓ أُنشئ المحفّز
✓ أُضيف صف واحد
✓ حُذف صف واحد
| op | product_id | qty |
|---|---|---|
| إضافة بند | 1 | 1 |
| حذف بند | 9 | 20 |
توقّع وتحقّق
لتوحيد البريد قبل حفظه نستخدم محفز:
في محفز DELETE، الصف المحذوف في:
TG_OP يحتوي:
في Oracle يُكتب الصف الجديد:
الخلاصة
- المحفز دالة
RETURNS triggerتعمل تلقائيًّا عندINSERTأوUPDATEأوDELETE. BEFOREيعدّلNEWأو يرفض؛AFTERللتسجيل والآثار الجانبية.OLDقبل التغيير، وNEWبعده.BEFOREيُرجعNEW(أوOLDفي الحذف)؛ إرجاعNULLيلغي العملية.
تحدٍّ برمجي: خصم المخزون تلقائيًّا
متقدماكتب محفزًا على 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; في آخرها.حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
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();