Hibernate وتخطيط الكيانات

سياق الاستمرارية وحالات الكيان

18 دقيقة الدرس 6 من 13

سياق الاستمرارية وحالات الكيان

حين تتولّى Hibernate إدارة كائناتك، يعيش كل كائن في حالة محدّدة بدقة بالنسبة إلى سياق الاستمرارية — مساحة العمل في الذاكرة التي تتتبّع جميع نسخ الكيانات المعروفة لدى EntityManager الحالي. إنّ فهم هذه الحالات ليس معلومة اختيارية: فهي من يقرّر ما إذا كانت تغييراتك ستُحفظ، أو تُتجاهل بصمت، أو تُسبّب استثناء تحميل كسول في بيئة الإنتاج.

ما هو سياق الاستمرارية؟

سياق الاستمرارية هو وحدة العمل بين كود التطبيق وقاعدة البيانات. يمكن تصوّره خريطةً من هوية الكيان (النوع + المفتاح الأساسي) إلى كائن Java حي. كل نسخة من EntityManager تمتلك سياق استمرارية واحدًا بالضبط. في تطبيق Spring Boot نموذجي، يكون نطاق السياق هو المعاملة الواحدة: يُفتح حين تبدأ المعاملة، ويُصرف ويُغلق حين تُودَع.

EntityManager مقابل سياق الاستمرارية: ليسا نفس الشيء. EntityManager هو الواجهة البرمجية التي تتعامل معها؛ أما سياق الاستمرارية فهو الذاكرة المؤقتة الداخلية التي يحتفظ بها. كل EntityManager لديه دائمًا سياق استمرارية واحد بالضبط، لكن يمكنك ضبط سياقات ممتدة تتجاوز حدود معاملة واحدة.

حالات الكيان الأربع

١ — عابر (Transient)

يكون الكائن في الحالة العابرة حين يُنشأ للتوّ بالمعامل new ولم يُرتبط قط بأي سياق استمرارية. لا تعلم Hibernate بوجوده، ولا يوجد صف قاعدة بيانات مقابله بعد، وإن تخلّيت عن المرجع فسيُجمع كقمامة دون أي تأثير جانبي.

import jakarta.persistence.*; @Entity @Table(name = "orders") public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String reference; // getters / setters محذوفة } // --- Order order = new Order(); // عابر — لا id، لا سياق order.setReference("ORD-2024-001"); // في هذه النقطة Hibernate لا تعلم بوجود هذا الكائن إطلاقًا.

٢ — مُدار (Managed / Persistent)

يكون الكائن مُدارًا حين يرتبط بسياق الاستمرارية الحالي. يحدث ذلك عبر em.persist(entity) للكائنات الجديدة، أو em.find()، أو em.merge()، أو استعلامات JPQL. ما دام الكيان مُدارًا تراقبه Hibernate؛ فقبيل إيداع المعاملة تقارن كل كائن مُدار بالصورة التي أخذتها عند التحميل — وهي عملية تُعرف بـفحص التعديلات (dirty checking) — وتُصدر تلقائيًا جمل UPDATE اللازمة.

import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @Service public class OrderService { @PersistenceContext private EntityManager em; @Transactional public Order createOrder(String ref) { Order order = new Order(); // عابر order.setReference(ref); em.persist(order); // مُدار الآن؛ INSERT سيُنفَّذ عند الصرف return order; } @Transactional public void updateReference(Long id, String newRef) { Order order = em.find(Order.class, id); // مُدار فورًا order.setReference(newRef); // لا حاجة لأي حفظ صريح! // Hibernate تكتشف التغيير وتُصدر UPDATE عند الإيداع } }
لا حاجة لاستدعاء save() داخل المعاملة. إذا جلبت كيانًا أو حفظته داخل المعاملة ذاتها فأي تغيير للحقول يُتتبّع تلقائيًا. لهذا السبب، save() في Spring Data JPA مطلوب فعليًا فقط للكيانات العابرة أو المنفصلة؛ استدعاؤه على كيان مُدار مسبقًا ليس له أثر.

٣ — منفصل (Detached)

يكون الكائن منفصلًا حين كان مُدارًا في السابق لكن سياق استمراريته أُغلق — انتهت المعاملة، أو استدعيت em.detach(entity) أو em.clear(). لا يزال الكائن يحتفظ بمفتاحه الأساسي وآخر قيم معروفة لحقوله، لكن Hibernate لم تعد تراقبه. التغييرات التي تُجريها على كائن منفصل تُتجاهل بصمت ما لم تُعد ربطه صراحةً.

@Transactional public Order loadOrder(Long id) { return em.find(Order.class, id); // المعاملة تُودَع هنا، يُغلق السياق — الكائن المُعاد أصبح منفصلًا } // في مكان آخر، خارج أي معاملة: public void processDetached(Order detachedOrder) { detachedOrder.setReference("UPDATED"); // التغيير غير متتبَّع — لا معاملة، لا سياق // لحفظ التغيير، أعد الربط بـ merge(): anotherTransactionalMethod(detachedOrder); } @Transactional public Order reattachAndSave(Order detached) { // merge() ينسخ حالة الكائن المنفصل إلى نسخة جديدة مُدارة Order managed = em.merge(detached); return managed; // استخدم هذا المرجع — detached لا يزال منفصلًا }
مشكلة شائعة مع الكيانات المنفصلة والتحميل الكسول: إذا حمّلت كيانًا يحتوي على علاقة كسولة التحميل داخل معاملة، ثم حاولت الوصول لتلك العلاقة خارج المعاملة (مثلًا في طبقة العرض)، ستُطلق Hibernate استثناء LazyInitializationException لأن سياق الاستمرارية أُغلق بالفعل. الحلول: اجلب العلاقة مبكرًا بـ JOIN FETCH، أو استخدم مسقطات DTO، أو اعتمد نمط Open-Session-in-View (مُفعَّل افتراضيًا في Spring Boot — لكن تفهّم مقايضاته قبل الاعتماد عليه في الإنتاج).

٤ — محذوف (Removed)

يكون الكائن في الحالة المحذوفة حين تستدعي em.remove(managedEntity) على كيان مُدار. يبقى في سياق الاستمرارية حتى إيداع المعاملة، حيث تُصدر Hibernate جملة DELETE. الوصول إلى حالة كيان محذوف قبل الإيداع صحيح تقنيًا لكنه نادرًا ما يكون ذا معنى.

@Transactional public void cancelOrder(Long id) { Order order = em.find(Order.class, id); // مُدار if (order != null) { em.remove(order); // محذوف — 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 والمطوّر الذي يعمل معها بكفاءة.