محفزات sqlite: تنفيذ SQL تلقائيًا
المحفّز (Trigger) عبارة عن كتلة SQL مخزّنة داخل قاعدة البيانات، تعمل تلقائيًا عند وقوع حدث معيّن على جدول محدّد. تكتبه مرّة واحدة، ويتكفّل SQLite بعدها بأمر "متى يُنفَّذ".
الصيغة العامة:
لم نكتب أمر INSERT صريحًا في جدول price_history، بل المحفّز هو من تكفّل بذلك. وكل تحديث لاحق للسعر سيُسجَّل بنفس الطريقة، سواء جاء من سطر الأوامر أو من سكربت أو من تطبيق.
تشريح جملة CREATE TRIGGER في sqlite
لنقرأ صيغة إنشاء المحفّز جزءًا جزءًا:
CREATE TRIGGER trigger_name
{ BEFORE | AFTER | INSTEAD OF } { INSERT | UPDATE [ OF column_list ] | DELETE }
ON table_name
[ FOR EACH ROW ]
[ WHEN condition ]
BEGIN
-- جملة واحدة أو أكثر
END;
- التوقيت (Timing) —
BEFOREيُنفَّذ قبل التغيير، وAFTERيُنفَّذ بعده، أماINSTEAD OFفيحلّ محلّ العملية (ويعمل مع العروض views فقط). - الحدث (Event) — العملية التي تُطلِق المحفّز. الصيغة
UPDATE OF col1, col2تُقصِر التشغيل على تعديلات أعمدة بعينها. - الجدول — الجدول الذي تراقبه.
FOR EACH ROW— SQLite لا يدعم سوى المحفّزات على مستوى الصف، لذا هذه العبارة ضمنية. يمكنك كتابتها للتوضيح فقط دون أي أثر فعلي.WHEN— شرط اختياري، فلا يُنفَّذ جسم المحفّز إلا إذا تحقّق.- الجسم (Body) — جملة أو أكثر بين
BEGINوEND، وكل جملة يجب أن تنتهي بفاصلة منقوطة.
هذه هي القواعد كاملة. أغلب محفّزات sqlite في الواقع لا تتجاوز خمسة إلى عشرة أسطر.
OLD و NEW في محفزات sqlite: الصف الذي يتغيّر
داخل جسم المحفّز، يوفّر لك SQLite صفّين وهميّين للوصول إلى البيانات:
NEW— الصف الجديد القادم. متاح في محفّزاتINSERTوUPDATE.OLD— الصف الموجود حاليًا. متاح في محفّزاتUPDATEوDELETE.
محفّز DELETE يملك OLD فقط، ومحفّز INSERT يملك NEW فقط، أما محفّز UPDATE فيملك الاثنين معًا.
الصف المحذوف اختفى من accounts، لكن بياناته انحفظت في deletions قبل ما يروح.
محفز BEFORE في sqlite: التحقق من الصف أو تعديله
محفزات BEFORE تشتغل قبل ما يتم تطبيق التغيير على القرص فعليًا، وهي مفيدة لرفع خطأ أو لتطبيع البيانات قبل تخزينها:
عملية INSERT الثانية تتوقف قبل أن تُكتب أي صفّ. الاستدعاء RAISE(ABORT, '...') يُلغي العبارة الحالية ويتراجع إلى ما قبلها مباشرة، أمّا RAISE(FAIL, ...) وRAISE(ROLLBACK, ...) وRAISE(IGNORE) فتمنحك تحكّمًا أدقّ بما يحدث بعد ذلك.
إذا كان هدفك مجرّد التحقّق من صحّة البيانات، فالأفضل الاعتماد على قيود CHECK؛ فهي تصريحية ويأخذها المُحسِّن (optimizer) في الحسبان. أمّا محفّز BEFORE فاستخدمه حين تحتاج القاعدة إلى مراجعة جداول أخرى أو إلى منطق لا يستطيع CHECK التعبير عنه.
شرط WHEN في trigger sqlite: المحفّزات المشروطة
تعمل عبارة WHEN كمُرشِّح يُحدّد أيّ تغييرات على الصفوف ستُشغّل جسم المحفّز فعلًا. يُقيَّم هذا الشرط لكلّ صفّ على حدة، بعد ربط OLD وNEW:
الطلب الأول لا يجتاز الشرط، أما الطلبان الآخران فيجتازانه. لو لم نضع جملة WHEN، لكانت كل عملية إدراج ستُكتب في جدول big_orders، وكنت ستضطر للتصفية عند القراءة بدلاً من ذلك.
محفز INSTEAD OF: جعل العرض قابلاً للكتابة
الـ Views في SQLite للقراءة فقط افتراضياً. محفز INSTEAD OF يعترض أي عملية كتابة على الـ view ويُنفّذ بدلاً منها كود SQL الخاص بك، وعادةً ما يُترجم ذلك إلى عمليات كتابة على الجداول الأصلية:
التطبيق يتعامل مع العرض (View) وكأنه جدول عادي، أما المحفّز فيتكفّل بتقسيم الاسم إلى first_name و last_name خلف الكواليس دون أن يشعر بذلك أحد.
عرض محفزات قاعدة البيانات وحذفها في sqlite
تُخزَّن المحفزات في الجدول sqlite_master جنباً إلى جنب مع الجداول والفهارس:
DROP TRIGGER IF EXISTS name; هي الصيغة الآمنة. وعند حذف جدول يحتوي على محفز، يُحذف المحفز تلقائياً معه — لا داعي لتنظيف أي شيء مسبقاً.
أخطاء شائعة يقع فيها المبتدئون
هناك بعض النقاط التي تُربك المطورين أول مرة:
- المحفز يعمل لكل صف، لا لكل عبارة. عبارة
UPDATEتُعدّل 1000 صف تُشغّل المحفز 1000 مرة. وإذا كان جسم المحفز ثقيلاً، فالحساب يتراكم بسرعة. - المحفزات تعمل ضمن المعاملة المحيطة بها. إذا تراجعت العبارة الخارجية (rollback)، تتراجع كتابات المحفز أيضاً. هذا عادةً هو السلوك المطلوب، لكنه يعني أن المحفز ليس حلاً لـ"سجّل هذا مهما حدث".
- المحفزات التكرارية معطّلة افتراضياً. المحفز الذي يُعدّل نفس الجدول لن يُعيد تشغيل نفسه ما لم تُفعّل
PRAGMA recursive_triggers = ON;. اتركها معطّلة إلا إذا كان لديك سبب محدد. - يمكن للتطبيق تجاوز المحفزات، لكن فقط بتجاوز قاعدة البيانات نفسها. ما دامت كل عمليات الكتابة تمر عبر SQLite، فالمحفز سيعمل. حتى أُطر الـ ORM التي ترسل دفعات SQL خام تُشغّل المحفزات.
- لا تُوزّع منطق الأعمال على محفزات كثيرة. فهي غير مرئية من موضع الاستدعاء — من يحاول تتبع "من أين جاء هذا الصف؟" سيضطر للبحث في
sqlite_master. استخدمها للأغراض العرضية المشتركة (سجلات التدقيق، الأعمدة المشتقة، جعل الـ views قابلة للكتابة)، واترك الباقي في كود التطبيق.
مثال واقعي: سجل تدقيق (Audit Log)
لنجمع ما سبق في مثال عملي — نتتبّع كل تغيير يحدث على جدول posts:
محفّز واحد يكفي للحفاظ على دقّة updated_at وكتابة سجلّ تدقيق في مكان واحد فقط. الكود التطبيقي الذي ينفّذ UPDATE لا يحتاج أن يعرف بوجود أيٍّ من هذين الأمرين أصلاً.
ما التالي: دعم JSON
تتولّى المحفّزات أتمتة ما يحدث حول صفوف الجدول. أمّا الميزة المتقدّمة التالية في SQLite فهي ما يمكنك تخزينه داخل الصف نفسه — أي JSON. يوفّر SQLite مجموعة كاملة من دوال JSON للاستعلام عن البيانات المهيكلة وتعديلها دون مغادرة بيئة SQL، وهذا موضوع الصفحة التالية.
الأسئلة الشائعة
ما هو المحفز (Trigger) في SQLite؟
المحفز هو كتلة من أوامر SQL تعمل تلقائياً عند وقوع حدث محدد على الجدول، مثل INSERT أو UPDATE أو DELETE. تُعرّفه مرة واحدة باستخدام CREATE TRIGGER، ثم يتولى SQLite تشغيله نيابة عنك كلما وقع الحدث. هذه هي الطريقة المثلى للاحتفاظ بسجلات تدقيق (Audit Log)، أو تحديث أعمدة مشتقة، أو فرض قواعد عمل دون الاعتماد على تذكّر التطبيق لها.
ما الفرق بين محفزات BEFORE و AFTER و INSTEAD OF؟
يعمل BEFORE قبل تطبيق التغيير على الصف، وهو مناسب للتحقق من البيانات أو تعديلها قبل حفظها. أما AFTER فيعمل بعد تنفيذ التغيير فعلياً، ويُستخدم عادة للتسجيل (Logging) أو لمزامنة جداول أخرى. بينما INSTEAD OF فلا يعمل إلا على العروض (Views)، وهو يستبدل العملية الأصلية بالكامل، مما يتيح لك جعل العرض قابلاً للكتابة.
كيف أشير إلى الصف الذي يجري تغييره داخل المحفز؟
تستخدم NEW.column للوصول إلى الصف الجديد في INSERT و UPDATE، و OLD.column للصف القديم في UPDATE و DELETE. محفزات INSERT ترى NEW فقط، ومحفزات DELETE ترى OLD فقط، أما محفزات UPDATE فترى الاثنين معاً. هذه المراجع مرتبطة بالصف الذي تتم معالجته في تلك اللحظة فقط.
كيف أعرض قائمة المحفزات أو أحذفها في SQLite؟
المحفزات مخزّنة في جدول sqlite_master، فيمكنك عرضها كلها بالأمر SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger';. ولحذف محفز، استخدم DROP TRIGGER trigger_name;، أو DROP TRIGGER IF EXISTS trigger_name; إن لم تكن متأكداً من وجوده. ولاحظ أن حذف الجدول يحذف معه جميع محفزاته تلقائياً.