المعاملات مع Spring Data
المعاملات مع Spring Data
كل عملية كتابة على قاعدة البيانات — سواء كانت استدعاءً واحدًا لـ save() أو سلسلة من عشرين عملية — يجب أن تُجيب في نهاية المطاف على سؤال واحد: ماذا يحدث إذا سار الأمر بشكل خاطئ في منتصف الطريق؟ الجواب هو المعاملات (Transactions). في Spring Data JPA، تُدار المعاملات بشكل تصريحي عبر الحاشية @Transactional، وفهم القواعد التي تفرضها — ومقابض الأداء التي تكشفها — هو ما يُفرّق بين الكود الذي يعمل فحسب والكود الصحيح تحت الحمل.
لماذا تهمّ المعاملات
المعاملة هي وحدة عمل إما تُكتمل بالكامل أو تُلغى بالكامل. بدونها، قد يُخلّف الفشل بين عمليتَي كتابة مرتبطتين قاعدةَ بيانات في حالة غير مكتملة لم تقصدها أي قاعدة أعمال قط. يلفّ دعم Spring للمعاملات اتصال JDBC في وكيل AOP: عند دخول الدالة المُحاشاة تبدأ معاملة (أو يُنضمّ إليها)؛ وعند عودة الدالة بشكل طبيعي تُؤكَّد؛ وعند انتشار استثناء منها تُلغى.
SimpleJpaRepository جميع دوال الكتابة (save وdelete وsaveAll وغيرها) بـ @Transactional وجميع دوال القراءة بـ @Transactional(readOnly = true). لا تحتاج إلى كتابة @Transactional الخاصة بك إلا حين تنسّق استدعاءات مستودعات متعددة داخل دالة خدمة واحدة.
الحاشية @Transactional
ضع @Transactional على دالة في باقة Spring المُدارة (أو على الفئة نفسها لتطبيقها على كل دالة عامة). يستبدل Spring الباقة بوكيل يعترض الاستدعاءات ويلفّها بمنطق المعاملات.
لأن عمليتَي الكتابة تشتركان في المعاملة ذاتها، فإن InsufficientStockException المرمي داخل inv.reserve(qty) سيُلغي وحدة العمل كاملةً — لن يصل أي تحديث جزئي لـ Inventory إلى قاعدة البيانات.
الانتشار: ما الذي يحدث حين تتداخل المعاملات
تتحكم خاصية propagation فيما يحدث حين تُستدعى دالة مُحاشاة من داخل معاملة أخرى. الافتراضي هو REQUIRED: الانضمام إلى المعاملة الخارجية إن وُجدت، وإلا بدء معاملة جديدة. أكثر القيم شيوعًا:
- REQUIRED (الافتراضي) — يشارك في المعاملة الموجودة؛ يبدأ واحدة إن لم تكن موجودة. استخدمه في غالب الأحوال.
- REQUIRES_NEW — يبدأ دائمًا معاملة جديدة ويُعلّق المعاملة الخارجية. مفيد لعمليات كتابة سجل التدقيق التي يجب أن تُؤكَّد حتى لو أُلغيت المعاملة الرئيسية.
- NOT_SUPPORTED — يُعلّق أي معاملة نشطة ويُنفّذ بدونها. نادر؛ بشكل رئيسي لاستعلامات التقارير الثقيلة القراءة على الجداول الكبيرة جدًا.
- MANDATORY — يجب استدعاؤه من داخل معاملة موجودة؛ يرمي استثناءً إن لم تكن هناك معاملة. مفيد للمساعدات الداخلية التي يجب ألا تُستدعى من خارج حدود الخدمة.
قواعد الإلغاء
بشكل افتراضي، يُلغي Spring المعاملة فقط على الاستثناءات غير المفحوصة (فئات RuntimeException والفئات الفرعية منها، بالإضافة إلى Error). لا تُشغّل الاستثناءات المفحوصة الإلغاء. يمكنك تجاوز هذا:
@Transactional دالةً أخرى @Transactional على نفس الباقة، يتم تجاهل الحاشية الثانية — يذهب الاستدعاء مباشرةً إلى this لا عبر الوكيل. استخرج الدالة الثانية إلى باقة Spring منفصلة للحصول على سلوك معاملة مستقل.
المعاملات للقراءة فقط
للدوال التي تقرأ البيانات فحسب ينبغي دائمًا تصريح @Transactional(readOnly = true). هذه العلامة الواحدة تحمل تبعات ذات مغزى:
- يُتخطّى فحص الاتساق لدى Hibernate. عند وقت التفريغ (flush)، تقارن Hibernate عادةً كل كيان مُدار بنسخته الاحتياطية لكشف التغييرات. مع
readOnly = trueتتخطى هذا الفحص كليًا، وهو أسرع بشكل ملحوظ حين تُحمِّل مجموعات كبيرة. - تلميح اتصال قاعدة البيانات. يمرر Spring تلميحًا للقراءة فقط إلى مشغّل JDBC. بعض قواعد البيانات (PostgreSQL، وإعدادات النسخ المتماثل في MySQL) تستطيع توجيه الاتصالات للقراءة فقط إلى نسخة ثانوية، مما يُخفّف الحمل عن الخادم الأساسي.
- يمنع الكتابات العَرَضية. إذا عدّلت الدالة كيانًا عن غير قصد، لن تُفرّغ Hibernate التغيير — مما يُساعدك على اكتشاف الأخطاء المنطقية مبكرًا.
readOnly = true كعادة. لا تكلفة إضافية حين تقرأ فحسب، لكنها توفّر على Hibernate مسح آلاف الكائنات المُدارة على خيط مشغول.
عزل المعاملات
تُعيّن خاصية isolation مستويات عزل SQL. الافتراضي هو DEFAULT الذي يُخبر Spring باستخدام ما تختاره قاعدة البيانات الأساسية (عادةً READ_COMMITTED لـ PostgreSQL وMySQL). يمكنك تشديد أو تخفيف هذا لكل دالة:
عمليًا، تعمل معظم التطبيقات بشكل جيد عند READ_COMMITTED. استخدم REPEATABLE_READ أو SERIALIZABLE فقط حين يكون لديك شذوذ تزامن محدد لمنعه وقد قست تكلفة الإنتاجية.
المهلة الزمنية
المعاملات طويلة الأمد تحتجز أقفال قاعدة البيانات واتصالات تجمّع الاتصالات. تُجبر خاصية timeout (بالثواني) على الإلغاء إذا لم تنته المعاملة في الوقت المحدد:
تجميع الأمور معًا: طبقة خدمة نموذجية
تحاشي الفئة بـ @Transactional(readOnly = true) ثم تجاوز دوال الكتابة الفردية بـ @Transactional العادية هو النمط المعياري. وهو صريح وآمن افتراضيًا ويُعظّم أداء القراءة.
الخلاصة
تُعدّ @Transactional الطريقة التي تُبقي بها Spring Data عمليات الكتابة ذرية وعمليات القراءة فعّالة. القواعد الأساسية: استخدم readOnly = true على كل دالة استعلام؛ الفّ عمليات المستودعات المتعددة في معاملة خدمة واحدة؛ تذكر أن الاستدعاء الذاتي يتجاوز الوكيل؛ واضبط قواعد الإلغاء حين تحتاج إلى أن تُشغّل الاستثناءات المفحوصة الإلغاء. بهذه العادات، ستكون طبقة البيانات صحيحة وسريعة معًا.