Spring Data JPA

الكيانات وأساسيات @Entity

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

الكيانات وأساسيات @Entity

في Spring Data JPA، الكيان الدائم (persistent entity) هو فئة Java عادية يُعيّنها Hibernate إلى جدول في قاعدة بيانات علائقية. يُعلَن عن هذا التعيين بالكامل عبر التعليقات التوضيحية (annotations) — دون XML ودون خطوة توليد كود. يسير هذا الدرس عبر كل ما تحتاجه لإنشاء كيان صحيح وجاهز للإنتاج: التعليقات المطلوبة وقواعد تسمية الأعمدة واستراتيجيات الهوية ومقايضات الأداء التي عليك فهمها قبل كتابة أول استدعاء لـ save().

الكيان الأدنى القابل للتطبيق

تعليقان توضيحيان إلزاميان لكل كيان: @Entity يُعلّم الفئة كنوع تديره JPA، و@Id يُحدّد الحقل الذي يُعيَّن إلى عمود المفتاح الأساسي. يستخدم Spring Boot 3 الحزمة jakarta.persistence (Jakarta EE 9 فأعلى)، وليس الحزمة القديمة javax.persistence.

package com.example.store.model; import jakarta.persistence.Entity; import jakarta.persistence.Id; @Entity public class Product { @Id private Long id; private String name; private double price; // تتطلب JPA منشئًا بلا وسيطات عامًا أو محميًا protected Product() {} public Product(Long id, String name, double price) { this.id = id; this.name = name; this.price = price; } // getters و setters ... }
لماذا المنشئ بلا وسيطات؟ يُنشئ Hibernate الكيانات بالانعكاس (reflection) عند تحميل الصفوف من قاعدة البيانات. يستدعي أولًا المنشئ بلا وسيطات ثم يضع قيمة في كل حقل. بدونه سيرمي مزوّد JPA استثناءً عند بدء التشغيل. ضعه كـ protected (لا public) لتثبيط كود التطبيق من استدعائه مباشرةً.

استنتاج اسمَي الجدول والعمود

يشتق Hibernate 6 افتراضيًا اسم الجدول من اسم الفئة واسم العمود من اسم كل حقل، وفق ImplicitNamingStrategy المُهيَّأة. يضبط Spring Boot هذه الاستراتيجية إلى SpringImplicitNamingStrategy التي تحوّل camelCase إلى snake_case: الحقل unitPrice يُصبح العمود unit_price.

استخدم @Table و@Column عندما تحتاج إلى تجاوز الإعدادات الافتراضية أو تقييد المخطط (schema):

import jakarta.persistence.*; @Entity @Table(name = "products", uniqueConstraints = @UniqueConstraint(columnNames = "sku")) public class Product { @Id private Long id; @Column(name = "product_name", nullable = false, length = 200) private String name; @Column(name = "unit_price", nullable = false) private double price; @Column(unique = true, length = 50) private String sku; }
اضبط دائمًا nullable = false على الأعمدة غير الاختيارية. يستطيع Hibernate توليد DDL (spring.jpa.hibernate.ddl-auto=validate في بيئة CI) وسيُنبّه إلى قيود NOT NULL المفقودة. هذا يكتشف انجراف المخطط مبكرًا بدلًا من وقت التشغيل.

استراتيجيات توليد المفتاح الأساسي

اختيار استراتيجية @GeneratedValue الصحيحة يؤثر على معدل إدخال البيانات وقابلية النقل وكيفية تجميع الإدراجات على دفعات. تُعرِّف JPA أربع استراتيجيات عبر مُعدَّد GenerationType:

  • AUTO (افتراضي): يختار Hibernate استراتيجية بناءً على dialect قاعدة البيانات. على PostgreSQL يستخدم تسلسلًا (sequence)؛ على MySQL يرجع إلى مولّد جدول. تجنّب AUTO في الإنتاج — قد يتغيّر سلوكه عند تبديل قواعد البيانات أو ترقية Hibernate.
  • IDENTITY: يُفوِّض إلى عمود التزايد التلقائي في قاعدة البيانات (SERIAL / AUTO_INCREMENT). بسيط، لكنه يُعطّل إدراج الدُّفعات عبر JDBC لأن Hibernate يجب أن يُفرغ كل صف منفردًا لاسترداد معرّفه المُولَّد قبل أن يتتالى إلى الكيانات التابعة.
  • SEQUENCE (موصى به لـ PostgreSQL / Oracle): يُخصّص كتلة من المعرّفات من تسلسل قاعدة البيانات في رحلة واحدة (allocationSize يتحكم في حجم الكتلة، افتراضيًا 50). يُعيّن Hibernate المعرّفات في الذاكرة ويُجمِّع عمليات INSERT بكفاءة.
  • TABLE: بديل محمول لكنه بطيء يستخدم جدول قفل مخصصًا. تجنّبه ما لم تكن قاعدة بياناتك تفتقر فعلًا إلى التسلسلات.
// الأفضل لـ PostgreSQL: تسلسل بكتلة تخصيص من 50 معرّفًا @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "product_seq") @SequenceGenerator(name = "product_seq", sequenceName = "product_id_seq", allocationSize = 50) private Long id;
// مريح لـ MySQL / MariaDB لكنه يُعطّل إدراج الدُّفعات @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
IDENTITY والدُّفعات لا يتوافقان. إن احتجت إلى إدراج آلاف الصفوف دفعةً واحدة (مثل استيراد البيانات بالجملة)، استخدم SEQUENCE أو خصّص المعرّفات يدويًا بـ UUID حتى يستطيع Hibernate التجميع. تفعيل spring.jpa.properties.hibernate.jdbc.batch_size=50 في application.properties لن يُجدي نفعًا ما دام IDENTITY مُستخدَمًا.

UUID كمفاتيح أساسية

في الأنظمة الموزّعة أو الواجهات البرمجية التي تريد فيها كشف معرّفات الكيانات دون الكشف عن التسلسل، يُعدّ مفتاح UUID الأساسي نمطًا شائعًا. يدعم Hibernate 6 بشكل أصيل java.util.UUID:

import java.util.UUID; import jakarta.persistence.*; import org.hibernate.annotations.UuidGenerator; @Entity @Table(name = "orders") public class Order { @Id @UuidGenerator // أصيل في Hibernate 6 — يولّد UUID من نوع v7 مستند إلى الوقت private UUID id; @Column(nullable = false) private String status; protected Order() {} }

يُفضَّل @UuidGenerator (خاص بـ Hibernate) على @GeneratedValue(strategy = GenerationType.UUID) (معيار JPA 3.1) لأنه يتيح التحكم في إصدار UUID. كلاهما يتجنّب رحلة ذهاب وإياب إلى قاعدة البيانات، لذا تُجمَّع عمليات الإدراج بشكل صحيح.

تعيينات الأنواع الأساسية للحقول

يُعيّن Hibernate أنواع Java القياسية إلى أنواع SQL تلقائيًا. التعيينات المهمة التي يجب معرفتها:

  • StringVARCHAR(255) افتراضيًا؛ تجاوز باستخدام @Column(length = N) أو @Lob للنصوص الكبيرة.
  • int / Integer وlong / LongINTEGER / BIGINT.
  • boolean / BooleanBOOLEAN (أو TINYINT(1) على MySQL).
  • java.time.LocalDate وLocalDateTime وInstant → أعمدة تاريخ/وقت أصيلة. لا حاجة لـ @Temporal في Hibernate 6.
  • BigDecimalDECIMAL؛ استخدمه دائمًا للقيم المالية — ولا تستخدم أبدًا double أو float في أعمدة مالية.
  • المُعدَّدات (Enums): علّق عليها بـ @Enumerated(EnumType.STRING) لتخزين الاسم ("ACTIVE") بدلًا من الترتيب (0)، الذي ينكسر إذا تغيّر ترتيب المُعدَّد.
import jakarta.persistence.*; import java.math.BigDecimal; import java.time.Instant; @Entity @Table(name = "products") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 200) private String name; @Column(nullable = false, precision = 10, scale = 2) private BigDecimal price; // لا تستخدم double للمبالغ المالية @Enumerated(EnumType.STRING) @Column(nullable = false, length = 20) private ProductStatus status; @Column(name = "created_at", nullable = false, updatable = false) private Instant createdAt; protected Product() {} }

equals() و hashCode() للكيانات

يضع Hibernate الكيانات في مجموعات Set ويقارنها أثناء فحص التغييرات (dirty checking). يمكن أن يُنتج الافتراضي Object.equals() (مقارنة الهوية) أخطاء خفية عندما يُحمَّل نفس صف قاعدة البيانات في جلستَي EntityManager منفصلتين. الأسلوب الموصى به هو بناء المساواة على المفتاح التجاري الطبيعي (مثل sku) بدلًا من المعرّف البديل، لأن المعرّف البديل يكون null قبل أول استدعاء لـ save().

@Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Product other)) return false; return sku != null && sku.equals(other.sku); } @Override public int hashCode() { // مستقر، لا يتغيّر حتى قبل الحفظ return getClass().hashCode(); }

الخلاصة

الكيان هو فئة Java عادية مُعلَّمة بـ @Entity وحقل @Id واحد. استخدم @Table و@Column لتوثيق المخطط وتقييده. اختر استراتيجية التوليد بعناية: SEQUENCE للإنتاجية، IDENTITY للبساطة، UUID للأنظمة الموزّعة. استخدم BigDecimal للمبالغ المالية، و@Enumerated(EnumType.STRING) للمُعدَّدات، وأنواع java.time الحديثة للتواريخ. عرّف equals() على مفتاح تجاري لا على المعرّف البديل. مع هذه الأسس جاهزةً ستكون مستعدًا لاستكشاف تجريد المستودع (repository) في الدرس التالي.