المعاملات في تطبيقات الويب
المعاملات في تطبيقات الويب
معاملة قاعدة البيانات (Transaction) هي سلسلة من جمل SQL يُعاملها محرك قاعدة البيانات باعتبارها وحدة عمل واحدة غير قابلة للتجزئة. إما أن تنجح كل جملة في المجموعة وتُسجَّل بصفة دائمة — وهو ما يُعرف بـ الالتزام (commit) — أو لا يسري أيٌّ منها وتُستعاد قاعدة البيانات إلى حالتها قبل تنفيذ أول جملة — وهو ما يُعرف بـ التراجع (rollback). هذا الضمان الذي تُقنّنه خصائص ACID هو ما يميّز تطبيق ويب موثوقًا عن تطبيق يترك البيانات في حالة غير متسقة عند حدوث أخطاء.
لماذا تحتاج كل عملية كتابة غير تافهة إلى معاملة
فكّر في عملية تقديم طلب في متجر إلكتروني: تحتاج إلى إدراج صف في جدول orders، وإدراج عدة صفوف في order_items، وتخفيض المخزون في products. إن فشل تحديث المخزون بعد كتابة الطلب بالفعل، ستحتوي قاعدة بياناتك على طلب وهمي لم يُخصم من المخزون. بدون معاملة، تُنتج عمليات الكتابة الجزئية بيانات تجارية فاسدة لا تُكتشف في الغالب إلا عند التدقيق.
الإعداد الافتراضي في JDBC: وضع الالتزام التلقائي
افتراضيًا، يبدأ كل Connection في JDBC في وضع الالتزام التلقائي (auto-commit): كل جملة هي معاملة صغيرة مستقلة تُلتزم فور تنفيذها. هذا مقبول للقراءة، لكنه يعني أن عمليات الكتابة متعددة الخطوات لا تحظى بأي حماية. أول شيء تفعله حين تحتاج معاملة حقيقية هو إيقاف الالتزام التلقائي:
من تلك النقطة، تتراكم الجمل في معاملة مفتوحة حتى تستدعي صراحةً commit() أو rollback(). استدعاء close() على اتصال لا يزال يحوي معاملة مفتوحة غير ملتزمة سيجعل المشغّل يتراجع تلقائيًا — لكن الاعتماد على ذلك خطأ لا ميزة.
النمط القياسي للمعاملة في طريقة Servlet أو Service
الهيكل الأمثل لعملية معاملة باستخدام JDBC الصرف يبدو على النحو التالي:
الهيكل متعمَّد: يُستدعى setAutoCommit(false) أولًا، ثم تُنفَّذ كل الجمل، ويستدعي catch على المستوى ذاته rollback() قبل إعادة الرمي. يُغلق try-with-resources الخارجي الاتصال بغض النظر عن النجاح أو الفشل.
معالجة طلب Servlet باعتباره وحدة عمل
في تطبيق ويب، الحد الطبيعي للمعاملة هو عادةً طلب HTTP واحد. النمط هو: افتح اتصالًا، ابدأ معاملة، أنجز كل عمل قاعدة البيانات الذي يتطلبه الطلب، التزم إن نجح كل شيء، تراجع إن رمى شيء استثناءً، ثم أعد الاتصال إلى التجمّع. يُسمى هذا أحيانًا نمط وحدة العمل (Unit of Work).
طريقة أنيقة لتطبيق ذلك دون تشتيت استدعاءات setAutoCommit / commit / rollback في طبقة DAO هي دفع حدود المعاملة إلى طريقة مساعدة تقبل lambda:
يمرّر المُستدعون lambda تستقبل اتصالًا مفتوحًا وجاهزًا للمعاملة بالفعل. منطق التراجع موجود في مكان واحد فقط:
نقاط الحفظ (Savepoints) — التراجع الجزئي
أحيانًا تريد التراجع عن جزء فقط من المعاملة — مثلًا، لإعادة محاولة عملية فرعية فاشلة دون فقدان الكتابات الناجحة السابقة. يدعم JDBC نقاط الحفظ (savepoints):
لا يدعم كل قاعدة بيانات أو مشغّل نقاط الحفظ بالتساوي؛ تحقق من وثائق قاعدة البيانات المستهدفة قبل الاعتماد عليها في المسارات الحرجة.
مستويات العزل — خريطة موجزة
يتيح لك JDBC ضبط مستوى العزل لكل اتصال عبر conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ). المستويات الأربعة القياسية من الأضعف إلى الأقوى هي:
- READ_UNCOMMITTED — يمكن قراءة بيانات معاملة أخرى غير ملتزمة (بيانات قذرة). نادرًا ما يكون مناسبًا.
- READ_COMMITTED — يقرأ البيانات الملتزمة فقط؛ الافتراضي في PostgreSQL وSQL Server. يمنع القراءة القذرة.
- REPEATABLE_READ — إعادة قراءة صف في نفس المعاملة تُعطي دائمًا النتيجة ذاتها؛ الافتراضي في MySQL/InnoDB. يمنع القراءة غير القابلة للتكرار.
- SERIALIZABLE — دلالات التنفيذ المتسلسل الكاملة؛ يمنع القراءات الوهمية لكنه قد يكون بطيئًا في ظل التزامن.
cfg.setTransactionIsolation("TRANSACTION_READ_COMMITTED") — اضبطه مرة واحدة هناك بدلًا من ضبطه لكل اتصال.
تمرير الاتصالات عبر طبقة DAO
خطأ شائع لدى المبتدئين هو جعل كل طريقة DAO تستدعي dataSource.getConnection() باستقلالية. حين تفعل ذلك، تعمل كل استدعاء DAO على اتصاله الخاص المنفصل — وبالتالي في معاملته الخاصة المنفصلة. لا يمكن أبدًا لاستدعاءات متعددة عبر DAOs مختلفة أن تكون ذرية. الحل هو تمرير الاتصال إلى طرق DAO من طبقة الخدمة حيث تُفتح المعاملة:
الخلاصة
كل عملية كتابة متعددة الخطوات في تطبيق ويب تنتمي إلى معاملة. النمط في JDBC هو: استدع setAutoCommit(false)، نفّذ جملك، استدع commit() عند النجاح، وrollback() في كتلة catch قبل إعادة الرمي. أغلّف هذا في أداة قابلة لإعادة الاستخدام (مساعد Tx.run() أو لاحقًا التوصيف @Transactional في Spring) حتى تعيش الميكانيكية في مكان واحد. مرّر الاتصال المفتوح إلى طرق DAO بدلًا من أن تفتح اتصالاتها الخاصة — هذه هي الطريقة الوحيدة التي تجعل استدعاءات DAO متعددة تشارك في نفس المعاملة. في الدرس القادم ستتعلم كيفية معالجة SQLException وتسريبات الموارد بأناقة عبر مكدس استدعاء JDBC كاملًا.