ذاكرة التخزين المؤقت من المستوى الأول
ذاكرة التخزين المؤقت من المستوى الأول
يحمل كل EntityManager في JPA ذاكرة تخزين مؤقت مدمجة تُسمّى ذاكرة التخزين المؤقت من المستوى الأول (المعروفة أيضًا بـذاكرة الجلسة أو ذاكرة سياق الاستمرارية). خلافًا لذاكرة التخزين المؤقت المشتركة بين الطلبات، تقتصر هذه الذاكرة على نسخة واحدة من EntityManager — فتُولد وتموت مع وحدة عمل واحدة. فهمها أمر ضروري لأنها تؤثر على كل استعلام تكتبه وكل كائن تقارنه وكل قرار أداء تتخذه.
ما هي ذاكرة التخزين المؤقت من المستوى الأول؟
يحتفظ Hibernate بخريطة هوية داخلية مُفهرَسة بنوع الكيان مضافًا إليه المفتاح الأساسي. في اللحظة التي تحمّل فيها كيانًا أو تثبّته أو تدمجه، يُسجّله Hibernate في هذه الخريطة. كل بحث لاحق عن النوع والمعرّف ذاتهما داخل EntityManager نفسه يُعيد الكائن Java المطابق تمامًا — دون أي رحلة إلى قاعدة البيانات ودون إنشاء نسخة ثانية.
هذه الذاكرة تعمل دائمًا. لا يوجد مفتاح إعداد لتعطيلها. إنها أساس ضمان هوية الكائن في JPA: ضمن سياق استمرارية واحد، يُمثَّل الصف نفسه في قاعدة البيانات دائمًا بالمرجع Java ذاته.
find() على نفس فئة الكيان والمفتاح الأساسي ضمن سياق استمرارية واحد يجب أن يُعيدا نفس نسخة الكائن. يُحقق Hibernate ذلك عبر ذاكرة التخزين المؤقت من المستوى الأول.
ضمان الهوية في التطبيق العملي
تأمّل هذا السيناريو داخل دالة خدمة Spring مُعلَّمة بـ@Transactional:
يُطلق Hibernate جملة SELECT واحدة عند استدعاء find() الأول. عند الاستدعاء الثاني يفحص خريطة الهوية فيجد إدخالًا لـ(Order, orderId) فيُعيد المرجع ذاته. لا تتولّد أي SQL. يمكنك التحقق من ذلك بتفعيل تسجيل SQL في application.properties:
التفاعل مع التحميل الكسول
لا تخزّن ذاكرة المستوى الأول الكيانات الجذرية فحسب. حين يُحلّل Hibernate ارتباطًا كسولًا يُخزَّن ناتجه أيضًا. إذا أشار كيانان مختلفان إلى الكيان المرتبط ذاته (مثلًا صفّان OrderItem يشتركان في Product واحد)، يُعيد Hibernate نسخة Product مشتركة واحدة بدلًا من إنشاء نسخة مكرّرة:
هذا ليس مجرد ميزة أداء. إنه يمنع حالات شذوذ الكتابة الضائعة: إذا عدّلت p1، يعكس p2 التعديل فورًا لأنهما الكائن ذاته في الذاكرة.
تجاوز الذاكرة: عندما تحتاج بيانات حديثة
بما أن ذاكرة المستوى الأول مقيّدة بـEntityManager الحالي، فهي لا تكون قديمة أبدًا بالنسبة للتعديلات التي أجريتها أنت في المعاملة ذاتها. غير أن معاملة أخرى على خيط مختلف ربما تعدّل الصف ذاته بالتوازي. إذا احتجت إلغاء الحالة المخزَّنة وإعادة التحميل من قاعدة البيانات، استخدم refresh():
refresh(). فهي تصل إلى قاعدة البيانات دائمًا وتتجاوز فحوصات الإصدار المتعلقة بالقفل التفاؤلي. احتفظ باستخدامها للحالات التي تعلم فيها يقينًا أن عملية خارجية غيّرت الصف — مثلًا بعد استدعاء إجراء مخزَّن أو مهمة دفعية تعمل خارج جلسة JPA.
إخراج إدخالات فردية من الذاكرة
يمكنك أيضًا إخراج كيان محدد من خريطة الهوية دون إعادة تحميله، باستخدام detach():
بعد detach()، لم يعد Hibernate يتتبّع التعديلات التي تطرأ على المرجع القديم عبر آلية الفحص للاتساخ. يدخل الكيان في حالة منفصل التي تناولها الدرس السابق.
مسح السياق كاملًا
في عمليات الدُّفعات — استيراد آلاف الصفوف في حلقة — تصبح ذاكرة المستوى الأول عبئًا. كل كيان تثبّته يتراكم في الذاكرة، ويتكرر فحص الاتساخ على جميعها وقت التدفق. النمط المعياري هو التدفق والمسح الدوري:
بعد clear()، تصبح خريطة الهوية فارغة. أي مرجع كيان احتفظت به قبل هذا الاستدعاء بات منفصلًا الآن. يُبقي هذا النمط استخدام الكومة ثابتًا بصرف النظر عن عدد الصفوف المعالَجة.
flush() وclear() في دُفعات، ولا تستخدم clear() وحدها. إذا مسحت دون تدفق أولًا، تُهمَل عمليات الإدراج أو التحديث المعلّقة ولا تُكتب إلى قاعدة البيانات. دائمًا دفّق قبل أن تمسح.
الذاكرة واستعلامات JPQL
ثمة دقة مهمة: استعلامات JPQL (وكذلك Criteria API) تذهب مباشرةً إلى قاعدة البيانات. إنها لا تستشير ذاكرة المستوى الأول قبل تنفيذ SQL. غير أنه بعد استلام Hibernate لصفوف النتائج يدمجها في خريطة الهوية. إذا كان أحد الصفوف المُعادة موجودًا مسبقًا في الذاكرة، يُعيد Hibernate النسخة المخزَّنة (متجاهلًا القيم الجديدة من نتيجة الاستعلام، إلا إذا استخدمت LockModeType.PESSIMISTIC_WRITE أو نفّذت refresh() صراحةً). قد يُفضي هذا إلى قراءات قديمة إذا عدّلت كيانًا ثم استعلمت عنه ضمن المعاملة ذاتها دون تدفق مسبق:
يُدفق Hibernate تلقائيًا قبل الاستعلامات في وضع FlushModeType.AUTO (الافتراضي) فقط حين يستهدف الاستعلام نوع كيان لديه تعديلات معلّقة. هذا صحيح في الغالب، لكن فهم الآلية يُساعدك على استيعاب نتائج الاستعلامات في المعاملات المعقّدة.
الخلاصة
ذاكرة التخزين المؤقت من المستوى الأول هي خريطة هوية إلزامية مرتبطة بـEntityManager تُلغي عمليات البحث المكرّرة في SQL وتضمن هوية الكائن داخل سياق الاستمرارية. استخدم refresh() لإجبار إعادة التحميل من قاعدة البيانات، وdetach() لإخراج كيان واحد، ونمط flush() + clear() لإبقاء عمليات الدُّفعات موفّرة للذاكرة. الوعي بكيفية تفاعل استعلامات JPQL مع الذاكرة — تجاوزها عند الخروج ودمج النتائج فيها عند العودة — ضروري لتفادي أخطاء القراءات القديمة التي يصعب تشخيصها.