JDBC في تطبيق الويب
JDBC في تطبيق الويب
تعرف بالفعل كيف تكتب دالة main() تفتح اتصال JDBC وتُنفّذ استعلامًا وتطبع النتيجة. تطبيق الويب شيء مختلف تمامًا؛ عشرات الطلبات تصل في آنٍ واحد وكل طلب يتوقع استجابة في أجزاء من الثانية. استدعاء قاعدة البيانات الذي بدا فوريًا في برنامج ذي خيط واحد يمكن أن يُدمّر معدل الأداء بصمت حين يُكرَّر مئات المرات في الثانية عبر خيوط متنافسة.
يفحص هذا الدرس بالضبط ما يتغير حين تنتقل JDBC من برنامج مستقل إلى طبقة الويب — ولماذا عزل كود قاعدة البيانات في طبقة منفصلة ليس خيارًا إن أردت صيانة التطبيق.
طبقة الويب متعددة الخيوط بطبيعتها
يُعيّن حاوي سيرفلت Jakarta EE (Tomcat أو Jetty أو WildFly) خيطًا مخصصًا لكل طلب HTTP وارد. مستخدمان يضغطان على نفس النقطة النهائية في اللحظة ذاتها يحصل كل منهما على خيط مستقل، وقد يحاول الخيطان استخدام قاعدة البيانات في اللحظة نفسها.
كائن java.sql.Connection غير آمن للخيوط المتعددة. إذا اشترك خيطان في نسخة واحدة من Connection فسيُفسد كل منهما جمل الآخر. الحل السطحي — حقل Connection ثابت في السيرفلت — هو فخ كلاسيكي في المقابلات يصل إلى الإنتاج في الواقع أكثر مما ينبغي.
Connection أبدًا كحقل في السيرفلت. السيرفلت كائن وحيد (singleton): نسخة واحدة تخدم جميع الخيوط. الاتصال المشترك يعني جمل SQL متشابكة ومجموعات نتائج مُشوَّهة واستثناءات يكاد يستحيل إعادة إنتاجها في بيئة الاختبار.
اتصال لكل طلب مقابل تجميع الاتصالات
النموذج الصحيح هو: اتصال واحد لكل وحدة عمل، يُستعار في بداية الطلب ويُغلق (يُعاد للمجموعة) قبل إرسال الاستجابة. ثمة نهجان:
- اتصال لكل طلب مع
DriverManager: يفتح مقبس TCP جديدًا لكل طلب. يعمل، لكن إنشاء الاتصال يكلف من 20 إلى 100 مللي ثانية، وهو ما يستهلك ميزانية وقت الاستجابة عند أي حمل يُعتدّ به. - تجميع الاتصالات (
DataSource): مجموعة ثابتة من الاتصالات المُسخَّنة مسبقًا تُشارَك بين جميع خيوط الطلبات. الاستعارة من المجموعة تستغرق ميكروثوانٍ. هذا هو النهج الوحيد القابل للتطبيق في الإنتاج.
المجموعة المعيارية في بيئة Java هي HikariCP. في تطبيق سيرفلت عادي تُهيّئها مرة واحدة في ServletContextListener وتعرضها عبر ServletContext:
ServletContextListener؟ يعمل مرة واحدة عند بدء تشغيل التطبيق (ومرة أخرى عند إيقافه). هذا هو الدورة الحياتية الصحيحة لمورد بالغ التكلفة كمجموعة اتصالات قاعدة بيانات — لا في كل مرة يعالج فيها سيرفلت طلبًا.
استعارة الاتصال داخل السيرفلت
بمجرد وجود DataSource في ServletContext، يمكن لأي سيرفلت استرجاعها واستعارة اتصال طوال مدة الطلب:
يضمن حقل try-with-resources إعادة الاتصال للمجموعة حتى عند حدوث استثناء — شرط مطلق في الكود متعدد الخيوط.
لماذا وضع SQL مباشرة في السيرفلتات يُشكّل مشكلة
السيرفلت أعلاه يعمل، لكنه يؤدي مهامًا كثيرة جدًا: يستقبل مدخلات HTTP وينفّذ SQL ويبني استجابة. هذه ثلاث مسؤوليات مستقلة في فئة واحدة. مع نمو التطبيق ستشعر بالألم:
- التكرار: استعلام
SELECT * FROM productsنفسه يظهر في خمسة سيرفلتات. تغيير اسم الجدول يعني تحرير خمسة ملفات. - صعوبة الاختبار: لاختبار أي منطق عمل يجب تشغيل حاوي سيرفلت وقاعدة بيانات. اختبارات الوحدة تتحول عن غير قصد إلى اختبارات تكاملية.
- الارتباط الوثيق: التبديل من MySQL إلى PostgreSQL يعني تتبع لهجات SQL عبر كل فئة متحكم.
الحل هو طبقة وصول بيانات منفصلة — مجموعة من الفئات التي وظيفتها الوحيدة هي الترجمة بين كائنات Java وصفوف قاعدة البيانات. كل جزء آخر من التطبيق يتحدث إلى هذه الفئات، لا إلى JDBC مباشرة.
نموذج الفصل بين المسؤوليات
في تطبيق ويب منظَّم جيدًا، تُوزَّع المسؤوليات عبر طبقات:
- طبقة الويب / المتحكم — السيرفلتات أو متحكمات الإطار. تُحلّل مدخلات HTTP وتستدعي الخدمة أو DAO وتُوجّه إلى العرض. لا SQL هنا أبدًا.
- طبقة الخدمة (اختيارية على النطاق الصغير) — قواعد الأعمال والتنسيق وحدود المعاملات. تستدعي DAO واحدًا أو أكثر.
- طبقة كائن الوصول للبيانات (DAO) — جمل JDBC وتحويل مجموعة النتائج. تُعيد كائنات نطاق أو قيمًا بدائية. لا معرفة بـ HTTP هنا.
- نموذج المجال — كائنات Java البسيطة (POJOs) كـ
ProductوOrder. لا تبعيات لأطر عمل أو JDBC.
مع هذا الهيكل يصبح السيرفلت رفيعًا بشكل لافت:
ProductDao (أو بشكل أفضل واجهة IProductDao) حتى تتمكن من استبداله بكائن وهمي في اختبارات الوحدة دون لمس السيرفلت. هذا هو أكبر مكسب للقابلية على الاختبار يمكنك إضافته لتطبيق JDBC.
ما تفعله JDBC — وما لا تفعله — في طبقة الويب
تبقى JDBC هي السباكة ذات المستوى المنخفض: سلاسل SQL وربط المعاملات ومجموعات النتائج والمعاملات. ما يتغير في سياق الويب هو كيفية إدارة دورة حياة الاتصال وأين تضع SQL. الآليات التفصيلية — PreparedStatement وتحويل الصفوف ومعالجة الاستثناءات — مغطاة في الدروس التالية. ما يهم الآن هو أن تفهم البنية قبل كتابة أول استعلام:
- تُهيَّأ المجموعة مرة واحدة وتعيش طوال عمر التطبيق.
- كل طلب يستعير اتصالًا وينجز عمله ثم يُعيده.
- SQL ينتمي إلى فئات DAO مخصصة لا إلى السيرفلتات أو صفحات JSP.
الخلاصة
نشر JDBC في تطبيق ويب يتطلب تغييرَين جوهريَّين عن الكود المستقل: استبدال DriverManager بـ DataSource مُجمَّع لتحمّل الحمل المتزامن، ونقل كود قاعدة البيانات كله خارج طبقة الويب إلى طبقة وصول بيانات مخصصة. هذان القراران — التجميع والتطبيق — هما الأساس الذي يبني عليه كل درس لاحق. مع وضوح البنية، يُضفي الدرس التالي صفة رسمية على طبقة الوصول للبيانات من خلال نمط DAO.