المعاملات للقراءة فقط والتحسين
المعاملات للقراءة فقط والتحسين
كل استدعاء لقاعدة البيانات يجري داخل معاملة (transaction) سواء طلبت ذلك أم لا. حين يقتصر الاستدعاء على قراءة البيانات، يصبح قفل الصفوف وتتبع الحالة المتغيّرة وإعداد سجل التراجع (rollback journal) مجرد عبء — عمل تؤديه قاعدة البيانات و Hibernate دون أي فائدة. يُعدّ الخيار readOnly في @Transactional الإشارة التي تتيح لكلا الطبقتين تخطّي هذا العمل والعمل بسرعة أكبر.
ما الذي يفعله readOnly فعلًا
يُشغّل تعيين @Transactional(readOnly = true) على إحدى الدوال تحسينات على طبقتين مستقلتين:
- طبقة Hibernate / JPA — يتحوّل سياق المثابرة (persistence context) إلى وضع الإخلاء NEVER. يعني ذلك أن Hibernate لن ينفّذ فحص الحالة المتغيّرة (dirty checking) عند الإخلاء: لن يفحص جميع الكيانات المدارة بحثًا عن تغييرات، ولن يحسب مجموعات التغييرات، ولن يولّد جمل UPDATE. تظل ذاكرة التخزين المؤقت من المستوى الأول تحمّل الكيانات لكنها تُعاملها كلقطات ثابتة طوال دورة حياة الدالة.
- طبقة JDBC / قاعدة البيانات — يمرّر Spring تلميح القراءة فقط إلى اتصال JDBC (
connection.setReadOnly(true)). تستخدم كثير من قواعد البيانات (PostgreSQL وMySQL InnoDB وOracle) هذا التلميح لتجنّب أقفال الكتابة على مستوى الصف، أو تخطّي إدخالات سجل التراجع، أو توجيه الاتصال إلى نسخة القراءة. يعتمد السلوك الدقيق على المشغّل وقاعدة البيانات، لكن الفائدة حقيقية وقابلة للقياس تحت الضغط.
readOnly = true تتخطى Hibernate هذا العمل كليًا. إن غيّرت كيانًا مُدارًا داخل معاملة للقراءة فقط عن طريق الخطأ، فلن يُحفظ التغيير بصمت تام — لا خطأ، فقط تحديث مفقود. ذلك خطأ برمجي وليس ميزة.
تعريف الدوال للقراءة فقط في خدمة
من الأنماط الشائعة وضع علامة @Transactional على مستوى الفئة كقراءة فقط، ثم تجاوز الدوال التي تُعدّل البيانات بالإعداد الافتراضي للكتابة:
يعمل التعليق التوضيحي على مستوى الفئة بوصفه إعدادًا افتراضيًا. أي دالة لا تملك @Transactional خاصًا بها ترث إعداد الفئة. أي دالة تحتاج إلى الكتابة تُضيف فقط @Transactional — الذي يكون افتراضيًا readOnly = false — لتجاوز الإعداد الافتراضي للفئة.
*QueryService (تُعلَّم الفئة بـ readOnly = true) و*CommandService (تستخدم معاملات الكتابة). يُبقي هذا الكودَ موثّقًا ذاتيًا، ويُسهّل توجيه القراءات إلى نسخة مخصصة لها، ويقلّل من خطر الكتابات العرضية في مسارات الاستعلام.
مستودعات القراءة فقط مع Spring Data JPA
يتيح Spring Data JPA الإعلان عن مستودع للقراءة فقط بتوسيع Repository (لا JpaRepository) وتعليق دوال الاستعلام:
بتوسيع واجهة علامة Repository الأساسية بدلًا من JpaRepository، لا تتوفر أي دوال للتعديل (save وdelete وغيرها) في هذه الواجهة، مما يُطبّق عقد القراءة فقط في وقت التصريف.
الإسقاطات: جلب ما تحتاجه فحسب
تتناسب معاملات القراءة فقط طبيعيًا مع الإسقاطات (projections). بدلًا من تحميل رسم بياني كامل للكيان في سياق المثابرة، يُعيد الإسقاط فقط الحقول التي يحتاجها المُستدعي. يُقلّل هذا من عدد الأعمدة التي يجلبها الاستعلام، ويُبقي كائنات الكيان أصغر حجمًا، ويتجنّب مفاجآت التحميل الكسول:
يستنتج Spring Data JPA قائمة SELECT من أسماء جالبات الواجهة. تجلب SQL المُولَّدة فقط id وstatus وtotal_amount — لا الصف الكامل من جدول ORDER ولا كل الجداول المرتبطة به.
قياس الأثر
تبرز مكاسب readOnly = true بوضوح في حالتين:
- رسوم بيانية كبيرة للكيانات: إذا حمّل استعلام واحد مئات الكيانات مع ارتباطاتها، فإن تخطّي فحص الحالة المتغيّرة عند الإخلاء يوفّر عددًا ملحوظًا من دورات المعالج لكل طلب.
- أعباء العمل ثقيلة القراءة: إذا كان 80–90% من حركة المرور قراءات — وهو الوضع الشائع في معظم خدمات الويب — فإن تحسين مسار القراءة يُضاعف مفعوله على الإنتاجية.
استخدم إحصاءات Hibernate أو أداة مثل Datasource-Proxy لتسجيل عدد جمل SQL لكل طلب. ستلاحظ في الغالب أن دالة خدمة ساذجة تُصدر استعلامات N+1 أو فحوص إخلاء زائدة يُلغيها readOnly = true مقترنًا بالإسقاطات كليًا.
نموذج لمخرجات الإحصاءات بعد تفعيلها — لاحظ أن عدد فحوص الحالة المتغيّرة يصل إلى الصفر في معاملات القراءة فقط:
المزالق الشائعة
- تعديل الكيانات داخل معاملة للقراءة فقط: كما أُشير أعلاه، تُلغى التغييرات بصمت. إذا رأيت حفظًا لا يُثبَّت، تحقق مما إذا كانت المعاملة المحيطة للقراءة فقط.
- استدعاء دالة للقراءة فقط من دالة كتابة في نفس الحبة (bean): لا يعترض AOP المستند إلى الوكيل في Spring الاستدعاءات الذاتية. إذا استدعت الدالة A (قراءة-كتابة) مباشرةً
this.methodB()(قراءة فقط) في نفس الفئة، تعمل methodB ضمن معاملة A لا ضمن معاملة جديدة للقراءة فقط. استخدم الانتشار (propagation) أو إعادة الهيكلة في حبات مختلفة لتجنّب ذلك. - افتراض أن جميع قواعد البيانات تحترم التلميح: تتجاهل H2 (المستخدمة شيوعًا في الاختبارات) تلميح القراءة فقط. قد يبدو الكود الذي يُعدّل كيانات داخل معاملة
readOnly = trueأنه يعمل في الاختبارات لكنه يفشل بصمت في الإنتاج.
readOnly = true لتعمل "أحيانًا"، فلديك خطأ برمجي كامن في الإنتاج. عَلِّم دائمًا دوال الخدمة التي تُعدّل البيانات صراحةً بـ @Transactional (الذي يكون قراءة-كتابة افتراضيًا)، وشغّل اختبارات التكامل على قاعدة بيانات حقيقية (PostgreSQL / MySQL) تُطبّق التلميح.
الخلاصة
لا يُعدّ @Transactional(readOnly = true) مجرد تلميح — بل هو عقد يُخبر Hibernate ومشغّل JDBC بالتخلّص من العمل غير الضروري. تُعطّل Hibernate فحص الحالة المتغيّرة والإخلاء، وتتجنّب قاعدة البيانات أقفال الكتابة وإدخالات التراجع، وتتحسّن الإنتاجية الإجمالية لمسارات القراءة. استخدمه كإعداد افتراضي في خدمات الاستعلام فقط والمستودعات، اقترنه بالإسقاطات لتقليل نقل البيانات، وانتبه للمزلقَين الرئيسيين: ضياع الكتابة بصمت، وتجاوز الاستدعاء الذاتي.