الإجراءات المخزّنة والقوادح والعروض
الإجراءات المخزّنة والقوادح والعروض منطقٌ يعيش داخل قاعدة البيانات بدلاً من التطبيق. الإجراء المخزّن (Stored Procedure) روتين له اسم يستدعيه التطبيق مع مُعامِلات (Parameters): فالاستدعاء CALL cancel_order(1042, 7) (الطلب 1042 يلغيه المستخدم 7) يفحص حالة الطلب ويلغيه ويعيد أصنافه إلى المخزون وينشئ مبلغاً مسترداً. والقادح (Trigger)، ويُسمّى أيضاً المُشغِّل، شيفرة مرتبطة بجدول تعمل من تلقاء نفسها عند تغيّر الصفوف، مثل كتابة صف تدقيق. والعرض (View) استعلام محفوظ تقرؤه التقارير كأنه جدول.
في درس القيود وسلامة البيانات اختبرت قواعد تصريحية مثل المفاتيح وCHECK. أما المنطق المخزّن فشيفرة إجرائية فيها فروع وآثار جانبية وأخطاء، لذا تختبره كأي شيفرة: مقابل عقد واضح، وبثلاث تقنيات لتصميم الاختبار، لكلٍّ منها درس خاص في وحدة تقنيات تصميم الاختبار. فتقسيم التكافؤ (Equivalence Partitioning) يقسم كل مُدخل إلى أقسام، وهي مجموعات قيم يُفترض أن يعاملها الروتين بالطريقة نفسها، ويختبر قيمة واحدة من كل قسم. وتحليل القيم الحدّية (Boundary Value Analysis) يختبر حواف الأقسام المرتبة: أصغر قيمة وأكبر قيمة في كل قسم، وأقرب القيم خارجهما مباشرةً. وجدول القرار (Decision Table) يسرد كل تركيبة من الشروط مع نتيجتها المتوقعة، فتصبح كل تركيبة ممكنة اختباراً.
لماذا يهم
من السهل أن يغيب منطق قاعدة البيانات عن الأنظار. فهو خارج شيفرة التطبيق التي تتجه إليها معظم المراجعات، ويتغيّر عبر سكربتات الترحيل (Migration)، والقادح يعمل بصمت في كل مسار يلمس جدوله: واجهة API، وتصحيح يجريه مسؤول، واستيراد ليلي. والعيب هناك يُتلف البيانات مباشرةً، كمبلغ مسترد يُنشأ مرتين، أو مخزون يُعاد لطلب سبق شحنه، أو تقرير يعدّ الطلبات مرتين، وكثيراً ما يظهر بعد أسابيع بعيداً عن سببه.
وهذه الروتينات تحمل قواعد حرجة: المال والمخزون وتغيّر الحالات وسجلات التدقيق. واستدعاؤها مباشرةً وفحص كل جدول تلمسه يتيحان لك اكتشاف هذه العيوب قبل الإصدار.
الإجراءات المخزّنة: عمليات تجري داخل قاعدة البيانات
يُنشأ الإجراء المخزّن في قاعدة البيانات، عادةً بسكربت ترحيل، ثم يُستدعى باسمه. ويأخذ مُعامِلات إدخال (IN) أو إخراج (OUT) أو INOUT، وقد يحتوي على فروع IF وحلقات وعدة عبارات INSERT وUPDATE وDELETE، وأخطاء صريحة مثل ORDER_NOT_CANCELLABLE. وتستخدم الفرق الإجراءات لإبقاء العملية متعددة الخطوات قرب البيانات، أو لتمكين التطبيق من تنفيذها دون صلاحية تعديل الجداول مباشرةً.
والدالة (Function) قريبة منه: تعيد قيمة وتُستخدم داخل استعلام، كما في SELECT order_total(1042). أما الإجراء فيُستدعى وحده بـCALL (في PostgreSQL وMySQL) أو بـEXEC (في SQL Server)، وفي PostgreSQL لا يستطيع التثبيت (Commit) أو التراجع (Rollback) داخل جسمه إلا الإجراء. عندما تقرأ أياً منهما، اكتب قائمة بما يلي:
- المُعامِلات وقيمها المسموحة: مدخلات لتقسيم التكافؤ وتحليل القيم الحدّية.
- كل فرع (
IFوCASEوالخروج المبكر): يحتاج كلٌّ منها إلى اختبار. - كل جدول يُقرأ أو يُكتب فيه، بما في ذلك عبر القوادح.
- كل خطأ يُطلق، وهل يتراجع الفشل في منتصف الطريق عمّا نُفّذ قبله.
-- PostgreSQL and MySQL: call a procedure
CALL cancel_order(1042, 7);
-- SQL Server (T-SQL)
EXEC dbo.cancel_order @order_id = 1042, @user_id = 7;
-- PostgreSQL and MySQL: a function returns a value and is used inside a query
SELECT order_total(1042);القوادح: شيفرة تعمل من تلقاء نفسها
ينتمي القادح إلى جدول وينطلق عند INSERT أو UPDATE أو DELETE. يعمل قادح BEFORE قبل كتابة الصف ويمكنه تعديله أو رفضه، ويعمل قادح AFTER بعدها ويناسب الآثار الجانبية مثل صفوف التدقيق. والقادح على مستوى الصف (FOR EACH ROW) يعمل مرة لكل صف متغيّر ويستطيع قراءة القيم OLD وNEW، أما القادح على مستوى العبارة فيعمل مرة لكل عبارة. ولا توجد في SQL Server قوادح BEFORE، بل AFTER وINSTEAD OF فقط، وقوادحه تعمل مرة لكل عبارة وترى كل الصفوف المتغيّرة في الجدولين inserted وdeleted، لذا تُخطئ الشيفرة التي تفترض صفاً واحداً عند التحديث الجماعي.
في محرّك يدعم المعاملات (Transactions)، الخطأ داخل القادح يُفشل العبارة التي أطلقته ويتراجع عن تغييراتها. وتتجاوز القوادح بعض المسارات: فعبارة TRUNCATE لا تطلق قوادح DELETE في PostgreSQL وMySQL وSQL Server، وفي MySQL لا يطلق التتابع (Cascade) عبر المفتاح الأجنبي أي قادح إطلاقاً. وفي PostgreSQL يتكوّن القادح من جزأين كما أدناه: دالة تُعرَّف بـRETURNS trigger، وعبارة CREATE TRIGGER تربطها بالجدول؛ ويستخدم شرط WHEN فيه IS DISTINCT FROM، وهي مقارنة «لا يساوي» تتعامل مع NULL تعاملاً صحيحاً. وفي قادح التدقيق هذا، اختبر أنه:
- ينطلق في كل مسار يغيّر الجدول: واجهة API، وشاشة الإدارة، والسكربت الجماعي، والاستيراد.
- لا يكتب صفاً حين تُضبط الحالة على قيمتها الحالية.
- ينطلق مرة واحدة لكل صف متغيّر، حتى حين تغيّر عبارة واحدة صفوفاً كثيرة.
- يسجّل الشخص الذي أجرى التغيير: مع اتصال تطبيق مشترك يكون مستخدم قاعدة البيانات هو حساب التطبيق، لذا يجب تمرير المستخدم كما يُمرَّر
updated_byهنا.
-- PostgreSQL: one audit row whenever an order's status really changes
CREATE FUNCTION log_status_change() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO order_audit (order_id, old_status, new_status, changed_by, changed_at)
VALUES (NEW.order_id, OLD.status, NEW.status, NEW.updated_by, now());
RETURN NEW;
END $$;
CREATE TRIGGER orders_status_audit
AFTER UPDATE OF status ON orders
FOR EACH ROW
WHEN (OLD.status IS DISTINCT FROM NEW.status)
EXECUTE FUNCTION log_status_change();العروض: استعلامات محفوظة تثق بها التقارير
العرض يخزّن استعلاماً لا بيانات: في كل قراءة تشغّل قاعدة البيانات الاستعلام على الجداول الأساسية الحالية. وتمنح العروض التقارير وواجهات API شكلاً ثابتاً، ويمكنها كشف بعض الأعمدة فقط، مثل بيانات العملاء دون أرقام الهواتف. أما العرض المُجسَّد (Materialized View) فيخزّن نتيجته ويعرض البيانات كما كانت عند آخر تحديث له، لذا تشمل اختباراته التحديث أيضاً.
عيوب العرض هي عيوب استعلام: نوع ربط خاطئ يُسقط صفوفاً، وربط بجدول فرعي يضاعفها، ومرشّح مفقود، وقيم NULL تختفي من العدّ. العرض أدناه يربط order_items، فيُعدّ الطلب الذي له صنفان مرتين.
- قارن العرض باستعلام مستقل على الجداول الأساسية، فهو مرجعك (Test Oracle).
- جهّز صفوفاً طرفية: طلب بعدة أصناف، وطلب ملغى، ويوم بلا طلبات، ومبلغ قيمته NULL.
- افحص الأعمدة أيضاً: البيانات الشخصية المخفية يجب ألا تعود تحت اسم آخر.
- في العرض المُجسَّد، قارن النتيجة قبل التحديث وبعده.
CREATE VIEW daily_sales AS
SELECT o.order_date, COUNT(*) AS order_count, SUM(o.total) AS revenue
FROM orders o
JOIN order_items i ON i.order_id = o.order_id
WHERE o.status <> 'CANCELLED'
GROUP BY o.order_date;
-- Seed: one order on 2026-09-14, total 190.00, with two item rows
-- Expected: order_count = 1, revenue = 190.00
-- Actual: order_count = 2, revenue = 380.00اختبار المنطق المخزّن: مثال تطبيقي
استخرج الاختبارات من عقد كل روتين: أقسام التكافؤ والقيم الحدّية للمُعامِلات، وجدول القرار حين تجتمع عدة شروط. واستدعاء الإجراء مباشرةً هو اختبار المكوّنات (Component Testing) بمصطلحات ISTQB، واستدعاؤه عبر واجهة API التي تستخدمه يختبر التكامل أيضاً. افعل الاثنين: فقد يمرّر التطبيق قيماً أخرى أو يخفي خطأ قاعدة بيانات واضحاً خلف خطأ محيّر.
كل اختبار تجهيز وتنفيذ وتحقق (Arrange-Act-Assert) في قاعدة بيانات اختبار: جهّز صفوفاً معروفة، ونفّذ الاستدعاء، ثم استعلم عن كل جدول يجب أن يتغيّر وكل جدول يجب ألا يتغيّر. إذا كان الإجراء لا يثبّت المعاملة بنفسه، فإنهاء الاختبار بـROLLBACK يعيد الحالة الأولى، وإلا فاستخدم قاعدة بيانات يمكنك إعادة ضبطها. وأطر مثل pgTAP (لـPostgreSQL) وtSQLt (لـSQL Server) تؤتمت هذا النمط.
المتطلب: «لا يُلغى الطلب إلا وهو PENDING أو PAID. الإلغاء يجعل الحالة CANCELLED ويعيد كل صنف إلى المخزون، والطلب PAID يحصل أيضاً على مبلغ مسترد واحد بقيمة إجماليه. وفي غير ذلك يطلق الاستدعاء ORDER_NOT_CANCELLABLE (أو ORDER_NOT_FOUND) ولا يتغيّر شيء».
الطلب المدفوع 1042: يُلغى
- التطبيق واجهة API للطلبات
- الإجراء المخزّن
cancel_order - الجداول الطلبات والمخزون والمبالغ المستردة والتدقيق
- قادح التدقيق
orders_status_audit
- 1 من التطبيق إلى الإجراء المخزّن: استدعاء الإجراء
CALL cancel_order(1042, 7) - 2 من الإجراء المخزّن إلى الجداول: قراءة الطلب الحالة PAID والإجمالي 190.00
- 3 من الإجراء المخزّن إلى الجداول: جعل الحالة CANCELLED
UPDATE orders - 4 من الجداول إلى قادح التدقيق: تغيّر الحالة يطلق القادح بعد التحديث، مرة واحدة للصف
- 5 من قادح التدقيق إلى الجداول: كتابة صف تدقيق واحد PAID → CANCELLED، المستخدم 7
INSERT INTO order_audit - 6 من الإجراء المخزّن إلى الجداول: إعادة الأصناف إلى المخزون المنتج 11: 50 → 52؛ المنتج 12: 8 → 9
UPDATE products - 7 من الإجراء المخزّن إلى الجداول: إنشاء مبلغ مسترد واحد قدره 190.00
INSERT INTO refunds - 8 من الإجراء المخزّن إلى التطبيق: تمّ بنجاح ردّ
النتيجة: تتغيّر أربعة جداول: يستعلم الاختبار عن كلٍّ منها، لا عن الرد وحده.
الطلب المشحون 1043: مرفوض
- التطبيق واجهة API للطلبات
- الإجراء المخزّن
cancel_order - الجداول الطلبات والمخزون والمبالغ المستردة والتدقيق
- قادح التدقيق
orders_status_audit
- 1 من التطبيق إلى الإجراء المخزّن: استدعاء الإجراء
CALL cancel_order(1043, 7) - 2 من الإجراء المخزّن إلى الجداول: قراءة الطلب الحالة SHIPPED
- 3 الإجراء المخزّن داخليًا: لا يُلغى إلا PENDING أو PAID
- 4 من الإجراء المخزّن إلى التطبيق: خطأ، دون أي تحديث ردّ
ORDER_NOT_CANCELLABLE
النتيجة: المتوقع: الجداول الأربعة كما كانت تماماً؛ فلا تتغيّر الحالة ولا ينطلق القادح أبداً.
- الطلب المدفوع 1042 (وحدتان من المنتج 11 ووحدة من المنتج 12، والإجمالي 190.00): CANCELLED، والمخزون 50 → 52 و8 → 9، ومبلغ مسترد واحد قدره 190.00، وصف تدقيق واحد PAID → CANCELLED باسم المستخدم 7.
- طلب PENDING: يُلغى ويعود المخزون، ويُكتب صف تدقيق واحد PENDING → CANCELLED، دون صف استرداد.
- الطلب المشحون 1043، والإلغاء الثاني للطلب نفسه، والطلب غير الموجود 9999: خطأ، والجداول الأربعة دون تغيير، ولا يُعاد المخزون مرتين أبداً.
- سطران للمنتج نفسه إن سمح المخطط بذلك: يرتفع المخزون بالكميتين معاً. والإجراء الذي يعيد المخزون بعبارة
UPDATE … FROM order_itemsواحدة في PostgreSQL، دون أن يجمع الكميات لكل منتج أولاً، سيطبّق سطراً واحداً فقط من السطرين، لأن صفّ الهدف الذي يطابق عدة صفوف مربوطة يُحدَّث مرة واحدة. - الفشل في المنتصف: إن كان على
refunds.order_idمفتاح فريد، فجهّز مبلغاً مسترداً للطلب 1042 كي تُرفض الخطوة الأخيرة، وهي إدراج المبلغ المسترد، واستدعِ الإجراء دون معاملة تغلّفه كما يفعل التطبيق. يجب أن تبقى الحالة والمخزون والتدقيق دون تغيير. - عبر واجهة API: الصفوف نفسها، ورد واضح من الفئة 4xx للطلب المشحون، لا 500 يحمل خطأ قاعدة البيانات الخام.
BEGIN;
-- Arrange: known rows (customer 317 exists; other columns use defaults)
INSERT INTO products (product_id, stock) VALUES (11, 50), (12, 8);
INSERT INTO orders (order_id, customer_id, status, total)
VALUES (1042, 317, 'PAID', 190.00);
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (1042, 11, 2), (1042, 12, 1);
-- Act
CALL cancel_order(1042, 7);
-- Assert: every table the procedure and its trigger touch
SELECT status FROM orders WHERE order_id = 1042; -- CANCELLED
SELECT product_id, stock FROM products ORDER BY product_id; -- 11: 52, 12: 9
SELECT amount FROM refunds WHERE order_id = 1042; -- 1 row: 190.00
SELECT old_status, new_status, changed_by
FROM order_audit WHERE order_id = 1042; -- 1 row: PAID, CANCELLED, 7
ROLLBACK;أخطاء شائعة
- فحص ما يعيده الإجراء فقط، لا كل جدول يغيّره هو وقوادحه، ولا الجداول التي يجب ألا تتغيّر.
- افتراض أن الإجراء يُنفَّذ كله أو لا شيء منه. هذا يعتمد على المحرّك وعلى معالجة الإجراء نفسه للمعاملات: افرض فشلاً متأخراً وانظر.
- اختبار القادح عبر الشاشة التي تطلقه عادةً فقط، دون عبارة جماعية أو استيراد أو تتابع.
- قبول العرض لأن مخرجاته تبدو معقولة، بدلاً من مقارنته باستعلام مستقل.
- تخطّي اختبار الانحدار حين يغيّر الترحيل وحده إجراءً أو قادحاً أو عرضاً: فهذا يظل تغييراً في الشيفرة.
من منظور ضمان الجودة
في جلسات التنقيح (Refinement) اسأل أين تُنفَّذ القاعدة: في التطبيق أم في قاعدة البيانات أم في كليهما. وإن كانت قاعدة البيانات طرفاً، فاحصل على صلاحية قراءة سكربتات الترحيل وراجع الشيفرة المخزّنة في طلب الدمج (Pull Request)؛ ففروعها وأخطاؤها تصبح شروط الاختبار لديك. واتفق مع المطور أو مسؤول قاعدة البيانات (DBA) على قاعدة اختبار يمكن إعادة ضبطها وسكربتات تجهيز يمكن إعادة تشغيلها.
سجّل كل اختبار مع صفوفه المجهّزة والاستدعاء بالضبط والصفوف المتوقعة لكل جدول واستعلامات التحقق، ليمكن تكراره وأتمتته. ويذكر تقرير الخطأ الاستدعاء، والصفوف قبله وبعده، والصفوف المتوقعة، ومحرّك قاعدة البيانات وإصداره، لأن الشيفرة المخزّنة وسلوك المعاملات فيها يختلفان بين المحرّكات والإصدارات.
تمرين عملي
صمّم اختبارات لقادح نقاط الولاء
المتطلب: «عندما يصبح الطلب DELIVERED يكسب العميل نقطة ولاء واحدة عن كل 10.00 ريالات كاملة من إجمالي الطلب. وإذا أصبح الطلب المسلَّم لاحقاً RETURNED، تُزال تلك النقاط». كتب المطور القادح أدناه.
صمّم حالات اختبار مع التغيّر المتوقع في loyalty_points، واكتشف عيوب الشيفرة.
CREATE FUNCTION award_points() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NEW.status = 'DELIVERED' THEN
UPDATE customers
SET loyalty_points = loyalty_points + floor(NEW.total / 10)
WHERE customer_id = NEW.customer_id;
END IF;
RETURN NEW;
END $$;
CREATE TRIGGER orders_award_points
AFTER UPDATE OF status ON orders
FOR EACH ROW
EXECUTE FUNCTION award_points();- اكتب تغيّرات الحالة التي يجب أن تضيف نقاطاً أو تزيلها أو لا تغيّر شيئاً.
- أضف القيم الحدّية لإجمالي الطلب.
- أضف أشكال العبارات والمسارات التي قد تطلق القادح بطريقة مختلفة.
- قارن كل حالة بالشيفرة.
إظهار الإجابة النموذجية
تغيّرات الحالة: SHIPPED → DELIVERED يضيف النقاط؛ وDELIVERED → RETURNED يزيلها؛ أما DELIVERED → DELIVERED (تحديث أُعيدت محاولته) والتغييرات التي لا تمسّ أياً من الحالتين، مثل PAID → SHIPPED، فلا تغيّر شيئاً.
الحدود: الإجمالي 9.99 ← 0 نقطة، و10.00 ← 1، و19.99 ← 1، و20.00 ← 2. افحص customers.loyalty_points قبل التغيير وبعده، لا مجرد غياب الخطأ.
الأشكال والمسارات: عبارة UPDATE واحدة تسلّم ثلاثة طلبات، اثنان منها لعميل واحد، تضيف نقاط الثلاثة؛ وتعطي واجهة API وشاشة الإدارة والسكربت الجماعي النتيجة نفسها.
العيوب: الحالة القديمة لا تُفحص أبداً، فيمنح DELIVERED → DELIVERED النقاط مرة أخرى (وعبارة WHEN (OLD.status IS DISTINCT FROM NEW.status) تمنع ذلك)؛ وRETURNED غير مُعالَجة، فلا تُزال النقاط أبداً؛ والطلب الذي يُدرج بالحالة DELIVERED، مثلاً بترحيل بيانات، لا يكسب عنه العميل أي نقاط لأن القادح ينطلق عند UPDATE فقط، فاسأل هل هذا مقصود.
أسئلة شائعة
هل اختبار الإجراءات المخزّنة مهمة المطور أم المختبِر؟
مهمة الاثنين. على المطورين كتابة اختبارات وحدة لشيفرة قاعدة البيانات، مثلاً بـpgTAP أو tSQLt. ويتحقق المختبِر من قاعدة العمل مقابل المتطلب، عبر التطبيق ومباشرةً حين تكون القاعدة معقدة، ويضيف الفحوص إلى حزمة الانحدار. والمنطق الذي لا يختبره أحد فجوة شائعة، فاسأل من يغطيه.
ما الفرق بين الإجراء المخزّن والدالة؟
الدالة تعيد قيمة وتُستخدم داخل استعلام، والإجراء يُستدعى وحده بـCALL أو EXEC ويمكنه إعادة قيم عبر مُعامِلات الإخراج. وفي PostgreSQL لا يستطيع التثبيت أو التراجع داخل جسمه إلا الإجراء. واختبرهما بالطريقة نفسها: المدخلات والمخرجات والصفوف المتغيّرة والأخطاء.
هل يخزّن العرض نسخة من البيانات؟
العرض العادي لا يخزّنها: يشغّل استعلامه على الجداول الأساسية في كل مرة يُقرأ فيها، فهو محدَّث دائماً. أما العرض المُجسَّد فيخزّن نتيجته ويعرض البيانات كما كانت عند آخر تحديث، لذا تختبر التحديث أيضاً.
كيف أختبر منطق قاعدة البيانات إن لم أستطع رؤية شيفرته؟
الشيفرة المخزّنة جزء من المخطط، فهي موجودة في سكربتات الترحيل في المستودع وفي قاعدة البيانات نفسها؛ فاطلب صلاحية القراءة. وإلى أن تحصل عليها، اختبرها كصندوق أسود: غيّر المدخلات وقارن الصفوف قبل كل استدعاء وبعده.
المراجع
- PostgreSQL Documentation: SQL Syntaxالبنية المعجمية لـ SQL في PostgreSQL: المعرّفات والاقتباس والثوابت النصية والتهريب.
- ISO/IEC 9075-1:2023 — Information technology — Database languages SQL — Part 1: Framework (SQL/Framework)الجزء الإطاري من معيار SQL الدولي، الذي تنفّذه لهجات PostgreSQL وMySQL وSQLite وغيرها بدرجات متفاوتة.
- ISTQB Certified Tester Foundation Level (CTFL) v4.0المنهج التأسيسي لاختبار البرمجيات: مستويات الاختبار وأنواعه وتقنيات تصميم الاختبارات وإدارة الاختبار والأدوات.
- Transactions — PostgreSQL Documentationما المعاملة: عدة خطوات تُطبَّق كلها أو لا شيء عبر BEGIN وCOMMIT وROLLBACK، ولا يراها الآخرون قبل التثبيت.
تدرّب بأدوات متقن
تنسيق استعلامات SQL بحالة أحرف متسقة للكلمات المفتاحية ومسافات بادئة وفواصل أسطر (MySQL وPostgreSQL وSQL Server وOracle وSQLite).
فحص جمل SQL بحثاً عن مشكلات الصياغة والأقواس غير المتوازنة والسلاسل النصية غير المغلقة والأنماط الخطرة.
توليد جمل INSERT من تعريف جدول أو صفوف نموذجية أو CSV أو JSON.
ولّد سكربتات تهيئة SQL (جملة CREATE TABLE وجمل INSERT) بصفوف اختبار واقعية لأي لهجة.
أنشئ جداول القرار من الشروط والإجراءات، وولّد جميع تركيبات القواعد، واختزل المستحيل منها.
نفّذ تحليلاً كاملاً للقيم الحدّية (ثنائي القيم وثلاثي القيم) مع فئات التكافؤ والنتائج المتوقّعة.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.