سياق الاستمرارية وحالات الكيان
سياق الاستمرارية وحالات الكيان
حين تتولّى Hibernate إدارة كائناتك، يعيش كل كائن في حالة محدّدة بدقة بالنسبة إلى سياق الاستمرارية — مساحة العمل في الذاكرة التي تتتبّع جميع نسخ الكيانات المعروفة لدى EntityManager الحالي. إنّ فهم هذه الحالات ليس معلومة اختيارية: فهي من يقرّر ما إذا كانت تغييراتك ستُحفظ، أو تُتجاهل بصمت، أو تُسبّب استثناء تحميل كسول في بيئة الإنتاج.
ما هو سياق الاستمرارية؟
سياق الاستمرارية هو وحدة العمل بين كود التطبيق وقاعدة البيانات. يمكن تصوّره خريطةً من هوية الكيان (النوع + المفتاح الأساسي) إلى كائن Java حي. كل نسخة من EntityManager تمتلك سياق استمرارية واحدًا بالضبط. في تطبيق Spring Boot نموذجي، يكون نطاق السياق هو المعاملة الواحدة: يُفتح حين تبدأ المعاملة، ويُصرف ويُغلق حين تُودَع.
EntityManager هو الواجهة البرمجية التي تتعامل معها؛ أما سياق الاستمرارية فهو الذاكرة المؤقتة الداخلية التي يحتفظ بها. كل EntityManager لديه دائمًا سياق استمرارية واحد بالضبط، لكن يمكنك ضبط سياقات ممتدة تتجاوز حدود معاملة واحدة.
حالات الكيان الأربع
١ — عابر (Transient)
يكون الكائن في الحالة العابرة حين يُنشأ للتوّ بالمعامل new ولم يُرتبط قط بأي سياق استمرارية. لا تعلم Hibernate بوجوده، ولا يوجد صف قاعدة بيانات مقابله بعد، وإن تخلّيت عن المرجع فسيُجمع كقمامة دون أي تأثير جانبي.
٢ — مُدار (Managed / Persistent)
يكون الكائن مُدارًا حين يرتبط بسياق الاستمرارية الحالي. يحدث ذلك عبر em.persist(entity) للكائنات الجديدة، أو em.find()، أو em.merge()، أو استعلامات JPQL. ما دام الكيان مُدارًا تراقبه Hibernate؛ فقبيل إيداع المعاملة تقارن كل كائن مُدار بالصورة التي أخذتها عند التحميل — وهي عملية تُعرف بـفحص التعديلات (dirty checking) — وتُصدر تلقائيًا جمل UPDATE اللازمة.
save() داخل المعاملة. إذا جلبت كيانًا أو حفظته داخل المعاملة ذاتها فأي تغيير للحقول يُتتبّع تلقائيًا. لهذا السبب، save() في Spring Data JPA مطلوب فعليًا فقط للكيانات العابرة أو المنفصلة؛ استدعاؤه على كيان مُدار مسبقًا ليس له أثر.
٣ — منفصل (Detached)
يكون الكائن منفصلًا حين كان مُدارًا في السابق لكن سياق استمراريته أُغلق — انتهت المعاملة، أو استدعيت em.detach(entity) أو em.clear(). لا يزال الكائن يحتفظ بمفتاحه الأساسي وآخر قيم معروفة لحقوله، لكن Hibernate لم تعد تراقبه. التغييرات التي تُجريها على كائن منفصل تُتجاهل بصمت ما لم تُعد ربطه صراحةً.
LazyInitializationException لأن سياق الاستمرارية أُغلق بالفعل. الحلول: اجلب العلاقة مبكرًا بـ JOIN FETCH، أو استخدم مسقطات DTO، أو اعتمد نمط Open-Session-in-View (مُفعَّل افتراضيًا في Spring Boot — لكن تفهّم مقايضاته قبل الاعتماد عليه في الإنتاج).
٤ — محذوف (Removed)
يكون الكائن في الحالة المحذوفة حين تستدعي em.remove(managedEntity) على كيان مُدار. يبقى في سياق الاستمرارية حتى إيداع المعاملة، حيث تُصدر Hibernate جملة DELETE. الوصول إلى حالة كيان محذوف قبل الإيداع صحيح تقنيًا لكنه نادرًا ما يكون ذا معنى.
مخطط انتقال الحالات
الحالات الأربع وانتقالاتها تشكّل دورة حياة محدّدة:
- جديد → مُدار:
em.persist(entity) - صف قاعدة بيانات → مُدار:
em.find()، استعلام JPQL،em.merge() - مُدار → منفصل: انتهاء المعاملة،
em.detach()،em.clear()،em.close() - منفصل → مُدار:
em.merge(detached)يُعيد نسخة مُدارة جديدة - مُدار → محذوف:
em.remove(entity) - محذوف → مُدار:
em.persist(removedEntity)قبل إيداع المعاملة
لماذا يؤثر ذلك على الأداء؟
يفحص dirty checking كل كيان مُدار وقت الصرف. إذا حمّلت معاملة واحدة مئات الكيانات، فعلى Hibernate مقارنة كل واحد منها — حتى تلك التي لم تنوِ تعديلها قط. هذه مشكلة أداء حقيقية في الإنتاج. استراتيجيات للتخفيف منها:
- استخدم معاملات للقراءة فقط (
@Transactional(readOnly = true)) للاستعلامات. تُخبر Spring تتجاهل Hibernate عملية dirty checking تمامًا. - فضّل مسقطات DTO (
SELECT new com.example.OrderDto(o.id, o.reference) FROM Order o) حين تحتاج البيانات فقط لا كيانات تنوي تعديلها. - استدعِ
em.detach(entity)صراحةً بعد قراءة كيان لن تُعدّله، لإزالته من فحص التعديلات.
@Transactional(readOnly = true) على كل توابع الخدمة التي تقرأ فقط. إنه تحسين أداء مجاني: Hibernate تتخطّى الصرف كليًا، وبعض مشغّلات JDBC وقواعد القراءة يمكنها تطبيق تحسينات إضافية.
أوضاع الصرف (Flush Modes)
لا تصرف Hibernate (تزامن السياق مع قاعدة البيانات) عند الإيداع فحسب. الوضع الافتراضي هو AUTO: تُصرف Hibernate أيضًا قبل تنفيذ استعلام JPQL إذا كانت التغييرات المعلّقة قد تؤثر على نتائجه. هذا صحيح لكنه قد يفاجئ المطوّرين الذين يتوقعون أن تبقى التغييرات غير مرئية حتى الإيداع. يمكنك تغيير وضع الصرف لكل EntityManager باستدعاء em.setFlushMode(FlushModeType.COMMIT)، لكن AUTO هو الافتراضي الآمن.
الخلاصة
كل كيان يكون دائمًا في إحدى الحالات الأربع: عابر (مجهول لـ Hibernate)، مُدار (متتبَّع للتغييرات)، منفصل (معروف بهويته غير متتبَّع)، أو محذوف (مُجدوَل للحذف). الانتقالات بين هذه الحالات تقودها عمليات EntityManager وحدود المعاملات. إتقان دورة الحياة هذه هو ما يفرق بين المطوّر الذي يُصارع Hibernate والمطوّر الذي يعمل معها بكفاءة.