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

JPA وHibernate ومدير الكيانات

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

JPA وHibernate ومدير الكيانات

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

JPA: المواصفة

JPA (Jakarta Persistence API) هي معيار — مجموعة من الواجهات والتعليقات التعريفية والقواعد المُعرَّفة في حزمة jakarta.persistence. لا تحتوي على أي كود تنفيذي؛ بل تُحدّد فقط الشكل الذي يجب أن يبدو عليه ORM. الفئات الرئيسية التي تتعامل معها يوميًا — @Entity، و@Id، وEntityManager، وTypedQuery — هي كلها واجهات أو تعليقات تعريفية تابعة لـ JPA.

بما أن JPA مجرد مواصفة، يمكن لأي مكتبة متوافقة تنفيذها. أكثر تطبيقَين شيوعًا هما Hibernate (الأكثر انتشارًا بفارق كبير) وEclipseLink. يستورد كودك من jakarta.persistence.*، لذا فإن التبديل بين المزوّدين لا يتطلب أي تغييرات في الكود — مجرد استبدال تبعية.

jakarta.persistence مقابل javax.persistence: انتقل Spring Boot 3 وHibernate 6 إلى فضاء أسماء Jakarta EE. جميع الاستيرادات تستخدم jakarta.persistence.*. إن رأيت javax.persistence.* في شرح ما فهو يستهدف مكدسًا أقدم (Spring Boot 2 / Hibernate 5). لا تخلط بينهما — التعليقات التعريفية تبدو متطابقة لكنها فئات مختلفة ولن يكتشفها ORM.

Hibernate: التطبيق

Hibernate هو المكتبة الفعلية التي تقوم بالعمل: تفحص فئاتك بحثًا عن تعليقات JPA التعريفية، وتولّد SQL، وتدير ذاكرة التخزين المؤقتة من المستوى الأول، وتترجم الاستثناءات، وتتكامل مع تجمّعات الاتصال. كما توفّر مجموعة من الامتدادات الخاصة بـ Hibernate (تعليقات في org.hibernate.annotations.* وواجهة Session) التي تتجاوز مواصفة JPA.

قاعدة عملية: فضّل تعليقات JPA التعريفية لكل ما تغطيه المواصفة، والجأ إلى امتدادات Hibernate الخاصة فقط حين لا تستطيع JPA التعبير عمّا تحتاجه (مثل أنواع SQL المخصصة، أو ضبط الجلب الدفعي). هذا يُبقي كودك قابلًا للنقل وأسهل في الفهم.

التبعية في Spring Boot 3: يسحب spring-boot-starter-data-jpa Hibernate 6 تلقائيًا. لا تحتاج إلى إضافة Hibernate مباشرةً. يُهيّئ المُشغِّل أيضًا DataSource (HikariCP)، وEntityManagerFactory، وإدارة معاملات Spring — كل ذلك من application.properties.

EntityManagerFactory وEntityManager

يُحمَّل وقت تشغيل JPA عبر كائنَين:

  • EntityManagerFactory (EMF) — كائن ثقيل آمن للخيوط المتعددة يُنشأ مرة واحدة لكل تطبيق (واحد لكل وحدة إدامة). يقرأ بيانات التعيين الوصفية ويتحقق من المخطط ويُعدّ قوالب SQL. إنشاؤه مكلف؛ لا تنشئه أبدًا لكل طلب.
  • EntityManager (EM) — كائن خفيف غير آمن للخيوط المتعددة يمثّل وحدة عمل واحدة. يمتلك سياق الإدامة (ذاكرة التخزين المؤقتة من المستوى الأول). تحصل على EM جديد لكل طلب أو معاملة، تستخدمه، ثم تغلقه.

في تطبيق Spring Boot لا تلمس EMF أو تنشئ EM يدويًا تقريبًا. تديرهما حاوية Spring وتحقن وكيل EM في حبّاتك عبر @PersistenceContext (أو بشكل غير مباشر عبر مستودعات Spring Data).

import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @Repository public class OrderRepository { @PersistenceContext private EntityManager em; // تحقن Spring وكيلًا آمنًا للخيوط @Transactional public void save(Order order) { em.persist(order); // INSERT عند التفريغ } public Order findById(Long id) { return em.find(Order.class, id); // SELECT بالمفتاح الأساسي — يتحقق من ذاكرة L1 أولًا } @Transactional public void updateStatus(Long id, String status) { Order order = em.find(Order.class, id); order.setStatus(status); // لا حاجة لـ update صريح — يعمل فحص التغييرات } // تفريغ عند إتمام المعاملة → UPDATE }
لماذا @PersistenceContext آمن للخيوط: تحقن Spring وكيلًا يُفوّض كل استدعاء إلى EM المرتبط بمعاملة الخيط الحالي. كائنات EM الفعلية قصيرة العمر ولا تُشارَك بين الخيوط.

عمليات EntityManager الأساسية

يعرض EM واجهة برمجية صغيرة ومتعامدة. فهم ما تفعله كل طريقة بقاعدة البيانات وبسياق الإدامة أمر بالغ الأهمية لتجنّب الاستعلامات المفاجئة ومشكلات الأداء.

  • persist(entity) — يجدول INSERT. ينتقل الكيان إلى حالة مُدار. يُؤجَّل SQL حتى التفريغ (عادةً عند إتمام المعاملة).
  • find(Class, id) — يُعيد الكيان المُدار لمفتاح أساسي محدد، أو null إن لم يوجد. يتحقق من ذاكرة المستوى الأول قبل الاستعلام عن قاعدة البيانات.
  • getReference(Class, id) — يُعيد وكيلًا (عنصرًا كسولًا) دون لمس قاعدة البيانات. مفيد لتعيين المفاتيح الأجنبية دون تحميل الكائن كاملًا. يرمي EntityNotFoundException عند أول وصول للحقل إذا لم يوجد الصف.
  • merge(entity) — ينسخ حالة كيان منفصل إلى نسخة مُدارة ويُعيد النسخة المُدارة. الكائن الأصلي يبقى منفصلًا. مطلوب عند تمرير الكيانات عبر حدود المعاملة أو الجلسة.
  • remove(entity) — يجدول DELETE. يجب أن يكون الكيان مُدارًا (مرّره عبر find أولًا إن لزم).
  • flush() — يُزامن سياق الإدامة مع قاعدة البيانات فورًا (ينفّذ SQL المعلّق) دون إتمام المعاملة.
  • detach(entity) / clear() — يُزيل كيانًا واحدًا أو جميع الكيانات من سياق الإدامة. مفيد في المعالجة الدفعية لتحرير الذاكرة.
// نمط التحديث المعتاد — لا توجد em.update() صريحة في JPA @Transactional public void applyDiscount(Long productId, int discountPercent) { Product p = em.find(Product.class, productId); // مُدار p.setPrice(p.getPrice() * (100 - discountPercent) / 100.0); // em.persist() غير مطلوب — يُطلَق فحص التغييرات عند التفريغ } // كيان منفصل قادم من جسم طلب REST @Transactional public Product updateFromDto(ProductDto dto) { Product detached = dto.toEntity(); // غير مُدار return em.merge(detached); // تُعيد نسخة مُدارة }

حالات دورة حياة الكيان

يوجد كل كيان JPA في إحدى أربع حالات بالنسبة لـ EntityManager:

  1. جديد (Transient) — مُنشأ بـ new، غير مرتبط بأي EM. لا يُتتبّع تغييراته.
  2. مُدار (Managed) — مرتبط بـ EM وسياق الإدامة. تُكتشف جميع التغييرات تلقائيًا وتُفرَّغ.
  3. منفصل (Detached) — كان مُدارًا لكن أُغلق EM أو استُدعيت detach(). لا يُتتبّع؛ استخدم merge() لإعادة الاتصال.
  4. محذوف (Removed) — مجدول للحذف؛ سيُطلَق DELETE عند التفريغ.
LazyInitializationException — أكثر مزالق Hibernate شيوعًا: إن وصلت إلى ارتباط يُحمَّل كسولًا على كيان منفصل (خارج المعاملة، مثلًا في قالب Thymeleaf)، يرمي Hibernate استثناء LazyInitializationException. الحل هو تحميل البيانات بينما الكيان لا يزال مُدارًا: إما باستخدام استعلام JOIN FETCH، أو استدعاء Hibernate.initialize()، أو اعتماد إسقاط DTO.

ملف persistence.xml والضبط التلقائي في Spring Boot

يتطلب JPA التقليدي ملف META-INF/persistence.xml لتعريف وحدة الإدامة. يستبدل Spring Boot هذا كليًا بضبط تلقائي مدفوع بـ application.properties:

# application.properties spring.datasource.url=jdbc:postgresql://localhost:5432/shop spring.datasource.username=appuser spring.datasource.password=${DB_PASS} spring.jpa.hibernate.ddl-auto=validate # تحقق من المخطط عند البدء؛ استخدم create-drop للاختبارات spring.jpa.show-sql=true # سجّل SQL المُولَّد (عطّله في الإنتاج) spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

يفحص Spring Boot جميع فئات @Entity على مسار الفئات تلقائيًا — دون الحاجة لإدراج صريح. يُنشئ فول EntityManagerFactory بواسطة HibernateJpaAutoConfiguration ويُعرَّض كـ LocalContainerEntityManagerFactoryBean.

الخلاصة

JPA تُعرِّف العقد وHibernate يُنفّذه. EntityManager هو واجهتك البرمجية الأساسية: احصل على واحد لكل معاملة (يتولى Spring ذلك)، واستخدم persist وfind وmerge وremove لإدارة دورة حياة الكيانات، ودع فحص التغييرات يتولى UPDATEs تلقائيًا. في الدرس التالي ستكتب أول فئة @Entity لديك وترى بالضبط كيف يُعيّن Hibernate حقولها إلى جدول في قاعدة البيانات.