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

مفاهيم ORM وهايبرنيت

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

مفاهيم ORM وهايبرنيت

قبل كتابة أي تعليق توضيحي (annotation) في هايبرنيت عليك أن تفهم لماذا يوجد أصلًا. يرسم هذا الدرس صورةً واضحة عن التعارض بين عالم البرمجة الكائنية وعالم قواعد البيانات العلائقية، ويشرح ما تفعله أُطر عمل ORM حيال هذا التعارض، ثم يوضّح موقع هايبرنيت في مكدّس Spring Boot 3 الحديث.

التعارض بين الكائنات والعلاقات

تُصوّر Java العالم بالكائنات: كائن Order يحتوي على مجموعة من كائنات OrderLine، كلٌّ منها يشير إلى كائن Product. في المقابل تُصوّر قواعد البيانات العلائقية العالم بالجداول والمفاتيح الخارجية. هذان التمثيلان مختلفان جوهريًا في خمسة أوجه يصطدم بها كلّ مطوّر في نهاية المطاف:

  • الحبيبية (Granularity): قد يُعيَّن كائن Java واحد (كـ Address) إما إلى جدوله الخاص أو إلى أعمدة إضافية داخل جدول آخر. لا يوجد تناظر واحد لواحد بالضرورة.
  • الهوية (Identity): تُفرّق Java بين المساواة المرجعية (==) والمساواة بالقيمة (equals). أما قاعدة البيانات فلها مفهوم هوية واحد فقط: المفتاح الأساسي. كائنان بالمفتاح ذاته يمثّلان الصف نفسه — لكن Java تتيح لك الاحتفاظ بنسختين مستقلتين دون أن تشكو.
  • العلاقات (Associations): علاقات Java هي مؤشرات اتجاهية مخزّنة في الذاكرة. العلاقات العلائقية هي مفاتيح خارجية في أعمدة — وللتنقّل بينها يلزم JOIN. علاقة ثنائية الاتجاه في Java تتطلب تنسيقًا؛ في قاعدة البيانات هي مجرد استعلامَين يشيران إلى المفتاح الخارجي ذاته.
  • الوراثة (Inheritance): تمتلك Java تسلسلات هرمية للفئات مع تعددية الأشكال. الجداول العلائقية لا تملك مفهومًا أصيلًا للوراثة. تعيين الأولى إلى الثانية يستوجب اختيار استراتيجية (جدول واحد بأعمدة قابلة للإهمال، جدول لكل فئة، جداول مُدمجة) وكلّ استراتيجية لها تكاليفها.
  • مشكلة n+1: تحميل قائمة من 100 كائن Order ثم الوصول إلى order.getLines() على كلٍّ منها يطلق 100 استعلام SQL إضافيًا بشكل ساذج. JDBC الخالص يتيح لك التحكّم بذلك بدقة؛ بدون عناية كافية يُخفي ORM هذه المشكلة حتى تراها في مقاييس الإنتاج.
التعارض ليس خللًا — بل هو فرق جوهري. صُمّمت النظرية العلائقية للإجابة عن استعلامات مخصّصة على مجموعات بيانات كبيرة مشتركة. بُنيت البرمجة الكائنية للتغليف وإعادة الاستخدام داخل عملية قيد التشغيل. يجسّر ORM الفجوة بينهما؛ لكنه لا يجعلهما متطابقَين.

ما يفعله ORM

إطار عمل ORM (Object-Relational Mapper) هو مكتبة تؤتمت الترجمة بين نموذج الكائنات ومخطط قاعدة البيانات. يتولّى أربع مهام ميكانيكية:

  1. توليد SQL: تستدعي entityManager.persist(order)؛ يُصدر ORM جملة INSERT الصحيحة بجميع الأعمدة المُعيَّنة.
  2. تعيين النتائج: يُنفّذ ORM جملة SELECT ويبني كائنات Java مُملّأة بالكامل من ResultSet وفق بيانات التعيين التي أعلنتها.
  3. اكتشاف التغييرات (Dirty Checking): عند تعديل كيان مُدار داخل معاملة، يكتشف ORM التغيير ويُصدر UPDATE تلقائيًا عند وقت المصافحة — دون أن تستدعي أي دالة حفظ.
  4. تحميل العلاقات: تُهيّئ ما إذا كانت الكائنات المرتبطة تُحمَّل فوريًا (في الاستعلام ذاته) أو بشكل كسول (عند الوصول الأول)، ويتولّى ORM معالجة الضمّ أو الاستعلامات الإضافية وفقًا لذلك.

ما لا يفعله ORM: تصميم مخططك، وضمان الأداء، أو كتابة استعلامات فعّالة نيابةً عنك. ما زلت بحاجة إلى فهم SQL والفهارس وخطط الاستعلام. يُزيل ORM الكود الرتيب (boilerplate) فحسب؛ التفكير لا يزال مسؤوليتك.

JPA مقابل هايبرنيت

يظهر هذان الاسمان معًا باستمرار، والتمييز بينهما مهم:

  • JPA (Jakarta Persistence API) هي مواصفة — مجموعة من الواجهات والتعليقات التوضيحية والعقود المُعرَّفة في حزمة jakarta.persistence. تُخبرك بشكل الواجهة البرمجية: @Entity وEntityManager و@OneToMany. لا تحتوي على كود قابل للتنفيذ.
  • هايبرنيت ORM هو التنفيذ المهيمن لتلك المواصفة. يُوفّر الكود الفعلي الذي يُثبّت الكائنات. حين تُضيف spring-boot-starter-data-jpa إلى مشروعك يسحب Spring Boot هايبرنيت 6 بوصفه مزوّد JPA.
اكتب كود التطبيق دائمًا في مواجهة واجهات JPA (EntityManager وTypedQuery والتعليقات التوضيحية القياسية). احتفظ بواجهات هايبرنيت المخصّصة (Session و@Formula و@BatchSize) للحالات النادرة التي تعجز JPA عن التعبير عنها. يُبقي ذلك الكود قابلًا للنقل وأسهل في الاختبار.

توجد مزوّدو JPA آخرون (EclipseLink وOpenJPA)، لكن هايبرنيت ظلّ الإعداد الافتراضي في Spring لأكثر من خمسة عشر عامًا وهو التنفيذ المرجعي الذي ستواجهه في كل مشروع حقيقي.

هايبرنيت في مكدّس Spring Boot 3

يستخدم Spring Boot 3 هايبرنيت 6 ويشترط Java 17+. التغيير الرئيسي في الحزم عن الإصدارات القديمة هو انتقال جميع تعليقات JPA التوضيحية من javax.persistence إلى jakarta.persistence — وهو تغيير يكسر التوافق ستصطدم به عند ترحيل الكود القديم. كل استيراد في هذا البرنامج التعليمي يستخدم الفضاء الجديد للأسماء.

التبعية النموذجية في مشروع Spring Boot 3:

<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>

يجلب ذلك المُبتدئ الواحد بشكل تعدّي: hibernate-core وjakarta.persistence-api وSpring Data JPA وSpring ORM وHikariCP. تحصل على مكدّس الثبات الكامل من سطر واحد.

أبرز إعدادات application.properties التي ستُهيّئها:

# application.properties # استراتيجية DDL: validate | update | create | create-drop | none spring.jpa.hibernate.ddl-auto=validate # تسجيل SQL الذي يولّده هايبرنيت (مفيد أثناء التطوير) spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true # يُكتشف اللهجة تلقائيًا في هايبرنيت 6؛ حدّده صراحةً فقط عند الحاجة # spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
لا تستخدم ddl-auto=update أو create في الإنتاج أبدًا. تتيح هذه الإعدادات لهايبرنيت تعديل مخططك المباشر، مما قد يُسقط الأعمدة أو يُغيّر القيود بصمت. استخدم validate في الإنتاج (يتحقّق من تطابق المخطط مع التعيينات دون تغيير أي شيء) وأدِر الترحيلات باستخدام Flyway أو Liquibase.

كيف يعمل هايبرنيت في وقت التشغيل

فهم نموذج وقت التشغيل يمنع المفاجآت. عند بدء تشغيل تطبيقك يقوم هايبرنيت بما يلي:

  1. يقرأ جميع الفئات المُعلَّمة بـ @Entity (مكتشَفة عبر مسح مكوّنات Spring).
  2. يبني SessionFactory (أو EntityManagerFactory بمصطلح JPA) — كائن ثقيل آمن للخيوط ومحدّد النطاق بالتطبيق يُنشأ مرة واحدة.
  3. لكل طلب أو وحدة عمل يفتح Session خفيف الوزن (EntityManager بمصطلح JPA) محدّد النطاق بمعاملة واحدة.
  4. يُديّر EntityManager ذاكرة التخزين المؤقت من المستوى الأول (سياق الثبات): خريطة من الكيانات المحمّلة مفهرسة بمفاتيحها الأساسية. يضمن ذلك أنك لن تحمّل الصف ذاته مرتين داخل معاملة واحدة وأن التغييرات يُتتبَّع تلقائيًا.

كيان Spring Boot بسيط ومستودع لتوضيح الصورة:

import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; @Entity @Table(name = "products") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int stockQuantity; // يشترط هايبرنيت مُنشئًا بدون وسيطات (يمكن أن يكون بصلاحية package-private) protected Product() {} public Product(String name, int stockQuantity) { this.name = name; this.stockQuantity = stockQuantity; } // getters وsetters محذوفة للاختصار }
import org.springframework.data.jpa.repository.JpaRepository; public interface ProductRepository extends JpaRepository<Product, Long> { // Spring Data يولّد التنفيذ عند بدء التشغيل }

حين تستدعي productRepository.save(product)، يستدعي Spring Data الأمر EntityManager.persist() الذي يُضيف INSERT في قائمة الانتظار. عند إتمام الدالة @Transactional المحيطة، يُصافح هايبرنيت SQL المعلّق إلى قاعدة البيانات. يكتب المطوّر صفر جمل SQL للعمليات الأساسية — لكن وراء كل خطوة في هذا المسار جملة SQL، وفهم تلك الجمل هو ما يُفرّق بين كود هايبرنيت جيد وكود هايبرنيت بطيء.

الخلاصة

التعارض بين الكائنات والعلاقات — الذي يشمل الحبيبية والهوية والعلاقات والوراثة ومشكلة n+1 — هو السبب الجذري لوجود أُطر عمل ORM. يُنفّذ هايبرنيت مواصفة JPA مُشكّلًا طبقة الترجمة بين رسم بياني لكائنات Java والمخطط العلائقي. يربط Spring Boot 3 كل شيء معًا تلقائيًا، لكن على المطوّر أن يفهم ما هي SQL التي تُولَّد ومتى ولماذا. تستكشف الدروس المتبقية في هذا البرنامج التعليمي كل تعليق تعيين وكل سلوك لهايبرنيت بعمق.