المعاملات في JPA وSpring
المعاملات في JPA وSpring
المعاملة (Transaction) هي وحدة عمل إما تكتمل بالكامل أو تترك قاعدة البيانات دون أي تغيير. بدون المعاملات، قد يُنتج عطل في منتصف عملية متعددة الخطوات — كخصم مبلغ من حساب بنكي قبل إيداعه في آخر — بيانات فاسدة وغير متسقة. المعاملات هي شبكة الأمان التي تمنع ذلك.
يجعل Spring وJPA معًا إدارة المعاملات شبه شفافة، إذ يخفيان عناء EntityTransaction.begin()/commit()/rollback() خلف تعليق توضيحي وحيد: @Transactional. يشرح هذا الدرس ما يفعله ذلك التعليق التوضيحي فعليًا، والضمانات التي يقدمها، والأنماط التي يجب على كل مطوّر في بيئة الإنتاج اتباعها.
ضمانات ACID
يُتوقع من كل معاملة في قاعدة بيانات علائقية استيفاء أربع خصائص، تُعرف مجتمعةً بـ ACID:
- الذرية (Atomicity) — تنجح جميع العمليات داخل المعاملة معًا أو لا تنجح أي منها. إذا رمى أي خطوة استثناءً، يُتراجع عن المعاملة بأكملها.
- الاتساق (Consistency) — تنقل المعاملة قاعدة البيانات من حالة صالحة إلى أخرى. يجب أن تظل القيود والمفاتيح الأجنبية والثوابت على مستوى التطبيق محققةً قبل المعاملة وبعدها.
- العزل (Isolation) — لا ترى المعاملات المتزامنة التغييرات المؤقتة غير المُلتزمة لبعضها البعض. درجة العزل قابلة للضبط (يُغطى ذلك في الدرس 3).
- الاستدامة (Durability) — بمجرد التزام المعاملة، تبقى تغييراتها راسخة في مواجهة الأعطال وانقطاع الطاقة وإعادة التشغيل. تكتب قاعدة البيانات إلى تخزين دائم (سجلات WAL وغيرها) قبل الإقرار بالالتزام.
@Transactional في Spring الذريةَ وعبر قاعدة البيانات الاتساقَ والاستدامةَ تلقائيًا. أما العزل فهو مفتاح ضبط على مستوى قاعدة البيانات يمكنك التحكم فيه — المزيد عن ذلك في الدرس 3.
كيف يُنفّذ Spring التعليق @Transactional
يلفّ Spring الـ bean الخاص بك في وكيل ديناميكي من JDK (أو وكيل فرعي CGLIB للفئات). عندما يستدعي مُستدعٍ ما دالةً مُعلَّمة بـ @Transactional، يعترض الوكيلُ الاستدعاءَ ويفتح معاملةً على EntityManager/DataSource، ثم يفوّض إلى دالتك، ثم يُلتزم أو يتراجع. لا يلمس كودك التجاري كائن المعاملة مباشرةً أبدًا.
لاحظ: إذا رُمي InsufficientStockException، فلن تصل لا تحديث المخزون ولا إدراج الطلب إلى قاعدة البيانات. هذه هي الذرية في العمل.
قواعد التراجع الافتراضية
يتراجع Spring عن المعاملة عند رمي استثناء غير مُتحقق (فئة فرعية من RuntimeException أو Error). أما عند تمرير استثناء مُتحقق فإنه يُلتزم — وهو قرار تصميمي قديم يُفاجئ كثيرًا من المطورين.
throws IOException وهرب IOException منها، فسيُلتزم Spring بالعمل الجزئي. استخدم @Transactional(rollbackFor = IOException.class) لتفعيل التراجع للاستثناءات المُتحققة — أو بشكل أفضل، لفّ الاستثناءات المُتحققة في أنواع فرعية من RuntimeException عند حدود النطاق.
يمكنك تخصيص سلوك التراجع عبر سمات التعليق التوضيحي:
EntityManager وسياق الاستمرار
عندما تُعلّم دالة مستودع Spring Data (أو الـ @Repository الخاص بك) بـ @Transactional، يربط Spring كائن EntityManager بالمعاملة الحالية. يحتفظ EntityManager بـ سياق الاستمرار (persistence context) — وهو خريطة هوية لكل كيان جرى تحميله. داخل معاملة واحدة، يُعيد تحميل نفس الصف مرتين مرجعَ كائن Java ذاته؛ لن يُصدر Hibernate استعلام SELECT ثانيًا.
عند الالتزام، تقارن آلية الفحص الدلالي للتغييرات (dirty-checking) في Hibernate الحالةَ الراهنة لكل كيان مُدار بلقطتها المأخوذة عند التحميل. أي حقل تغيّر يُطلق تحديثًا تلقائيًا — لا تحتاج إلى استدعاء save() صراحةً للكيانات التي استرجعتها في المعاملة ذاتها.
الاستدعاء الذاتي — الفخ الأكثر شيوعًا
نظرًا لأن @Transactional يُنفَّذ عبر وكيل، فإن استدعاء دالة @Transactional من داخل الفئة ذاتها يتجاوز الوكيل كليًا وبالتالي لا يُنتج أي أثر معاملاتي. هذه هي الثغرة الأكثر شيوعًا في معاملات Spring.
الحل: استخرج buildSummary() إلى bean مُدار بـ Spring منفصل وحقن ذلك الـ bean. حينئذٍ تمر الاستدعاءات عبر الوكيل وتُطبَّق دلالات المعاملة بصورة صحيحة.
موضع @Transactional — طبقة الخدمة مقابل طبقة المستودع
ضع التعليق على طبقة الخدمة لا على طبقة المستودع فحسب. دوال المستودع كـ save() وfindById() تحمل @Transactional الخاص بها من Spring Data، غير أن دالة خدمة تستدعي مستودعَين تحتاج إلى معاملة خارجية حتى تشترك كلتا العمليتين في وحدة العمل ذاتها. إذا أغفلت التعليق على مستوى الخدمة، تُلتزم كل استدعاء لمستودع بشكل مستقل — وتفقد الذرية عبرهما.
الخلاصة
ضمانات ACID هي العقد الذي تُوفّيه قاعدة البيانات لكل معاملة. يوفر وكيل @Transactional في Spring الذريةَ بشفافية: يُلتزم عند العودة النظيفة، ويتراجع عند الاستثناءات غير المُتحققة (وعند المُتحققة فقط عند الاختيار الصريح). يُغنيك الفحص الدلالي لسياق الاستمرار عن استدعاءات save() الصريحة، لكن الاستدعاء الذاتي يكسر الوكيل بصمت — احرص دائمًا على حقن bean منفصل عند الحاجة إلى تركيب المعاملات. أبقِ المعاملات قصيرة قدر الإمكان لتقليل التنافس على الأقفال. يتعمق الدرس التالي في الانتشار: ما الذي يحدث حين تستدعي دالة معاملاتية دالةً معاملاتية أخرى.