معالجة استثناءات SQL وإدارة الموارد
معالجة استثناءات SQL وإدارة الموارد
من أكثر مصادر الأخطاء شيوعًا في كود JDBC اثنان: الموارد غير المغلقة والاستثناءات المُبتلَعة صامتًا. فكائن Connection أو PreparedStatement أو ResultSet الذي لا يُغلَق أبدًا يُسرّب واصفات الملفات (file descriptors) ومؤشرات جانب قاعدة البيانات، مما يُدهور التطبيق أو يُعطّله تحت الحمل. أما الاستثناء الذي يُمسَك ثم يُتجاهل بصمت فيُحوّل خطأً واضحًا إلى لغزٍ محير. يغطي هذا الدرس الأنماط الصحيحة — وحقيقة المقايضات — حتى تكون طبقة DAO لديك متينة في بيئة الإنتاج.
ما تحمله SQLException
java.sql.SQLException استثناء محقَّق (checked) يلفّ ثلاث قطع تشخيصية مميّزة تتجاوز نص الرسالة:
- SQL State — كود من خمسة أحرف يحدّده معيار SQL (X/Open أو ISO). يُعرّف الحرفان الأولان فئة الخطأ:
08= فشل الاتصال،22= استثناء بيانات،23= انتهاك قيد تكاملية،42= خطأ صياغة أو انتهاك صلاحية. - Error Code — رقم صحيح خاص بالمورّد (مثلًا MySQL
1062= إدخال مكرر، PostgreSQL0إن كان مجهولًا). مفيد حين تحتاج إلى استرداد خاص بقاعدة بيانات معينة. - الاستثناءات المسلسلة — تعيد
getNextException()استثناءاتSQLExceptionإضافية سلسلها المشغّل على الأول. كرّر المشي في السلسلة دائمًا عند التسجيل.
23000 أو 23505 بدلًا من أكواد الخطأ الخاصة بالمورّد التي تختلف بين قواعد البيانات.
فئات فرعية من SQLException تستحق المعرفة
منذ Java 6، تُعرّف JDBC عدة فئات فرعية مُكتَّبة تتيح لك التقاط فئات محددة دون فحص سلاسل SQLState:
SQLTransientException— قد تنجح العملية عند إعادة المحاولة (مثلSQLTransientConnectionExceptionلخلل شبكة عابر).SQLNonTransientException— إعادة المحاولة دون إصلاح السبب الجذري لن تُجدي (مثل خطأ في صياغة SQL أو انتهاك قيد).SQLIntegrityConstraintViolationException— فئة فرعية منSQLNonTransientException؛ تُرمى عند انتهاك قيود الفريد أو المفتاح الأجنبي أو Check.SQLTimeoutException— تجاوز الاستعلام المهلة المحددة عبرsetQueryTimeout().BatchUpdateException— تُرمى حين يفشل أحد العبارات في استدعاءexecuteBatch()؛ تكشفgetUpdateCounts()أي الصفوف نجحت.
try-with-resources: النمط الوحيد المقبول
قبل Java 7، كان كود JDBC يتطلب كتل finally متداخلة لضمان إغلاق كل مورد حتى عند رمي استثناء في منتصف العملية. كان ذلك الكود مطوّلًا بشكل ملحوظ وسهل الخطأ — فمن الأخطاء الشائعة إغلاق ResultSet فقط مع نسيان Statement. تُزيل try-with-resources هذه المشكلة كليًا.
أي كائن تُنفّذ فئته java.lang.AutoCloseable يمكن إدراجه في قائمة موارد عبارة try. كلٌّ من Connection وStatement وPreparedStatement وResultSet تُنفّذ AutoCloseable. تُغلَق الموارد بترتيب عكسي لترتيب التصريح — ResultSet أولًا ثم PreparedStatement ثم Connection — بصرف النظر عمّا إذا أُلقي استثناء.
ResultSet بعد إعداد العبارة وربط المعاملات، لذا لا يمكنه مشاركة قائمة الموارد الخارجية دون تهيئة مزعجة بقيمة null. الحل الأنظف هو try (ResultSet rs = ps.executeQuery()) المتداخلة التي تجعل دورة حياة كل كائن واضحة تمامًا.
الاستثناءات المكبوتة (Suppressed Exceptions)
تعالج try-with-resources حالةً خفيةً كان نمط finally القديم يُخطئ فيها: إذا رمى جسم try استثناءً وأيضًا رمت close() استثناءً، فإن Java تُرفق استثناء الإغلاق كـاستثناء مكبوت على الأصلي عوضًا عن استبداله. يحفظ هذا الخطأ الأصلي — الشيء الذي تحتاج فعلًا لتشخيصه — مع تسجيل فشل الإغلاق أيضًا.
النمط المضاد لما قبل Java 7 (ولماذا فشل)
للسياق التاريخي، إليك النمط الهشّ الذي حلّت try-with-resources محلّه. لاحظ كيف يمكن لفحص null منسي واحد أو استثناء داخل finally أن يُخفي الخطأ الأصلي أو يترك موردًا مفتوحًا:
ignored مجرّد. سجّلها على الأقل بمستوى WARN. الاستثناءات المُصمَتة تجعل حوادث الإنتاج أصعب تشخيصًا بكثير وقد تشير إلى مشكلات خطيرة كتسريب في التجمّع أو معاملة فاسدة.
تغليف SQLException في استثناء وقت تشغيل
لأن SQLException استثناء محقَّق، فإنه يُسرّب تفاصيل قاعدة البيانات إلى كل طبقة في تطبيقك إن سمحت له بالانتشار بلا غطاء. يُغلّفه نمط DAO المعياري في استثناء غير محقَّق خاص بالمجال (domain-specific) قبل عبوره حدود DAO:
تلتقط طبقة الخدمة DataAccessException (أو تتركها تنتشر إلى Servlet أو Controller) دون أن تعتمد على java.sql إطلاقًا. هذا يُبقي اختيار تقنية قاعدة البيانات مخفيًا خلف واجهة DAO — وهو هدف محوري للنمط.
ضبط مهل العبارات والاستعلامات
استعلام جامح يمكنه الإمساك باتصال من التجمّع لدقائق، مما يُجفّف الطلبات الأخرى. اضبط دائمًا queryTimeout على أي Statement يُنفّذ منطقًا مدفوعًا بالمستخدم:
الخلاصة
تقوم إدارة موارد JDBC المتينة على ثلاث عادات: استخدم try-with-resources دائمًا لكل Connection وStatement وResultSet؛ وسجّل سلسلة الاستثناء الكاملة بما فيها SQLState وكود الخطأ والاستثناءات المكبوتة؛ وغلّف SQLException دائمًا في استثناء غير محقَّق خاص بالمجال قبل خروجه من طبقة DAO. طبّق الفئات الفرعية المُكتَّبة (SQLIntegrityConstraintViolationException، SQLTransientConnectionException) لمعالجة أوضاع الفشل المحددة بأناقة دون تحليل السلاسل النصية. في الدرس القادم ستُضيف طبقة خدمة فوق DAO تُطبّق قواعد الأعمال وحدود المعاملات.