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

تعيين الوراثة

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

تعيين الوراثة

يلجأ التصميم الكائني الطبيعي إلى الوراثة لتمثيل التسلسلات الهرمية في العالم الحقيقي: نوع أساسي Payment مع فئات فرعية ملموسة CreditCardPayment وBankTransferPayment وCryptoPayment. غير أن قواعد البيانات العلائقية لا تمتلك مفهومًا أصيلًا للأنواع الفرعية. تجسر JPA هذه الهوّة عبر ثلاث استراتيجيات مختلفة لتعيين الوراثة، لكل منها تبادلات قابلة للقياس في تعقيد المخطط وأداء الاستعلام وانضباط الأعمدة غير القابلة للقيم الخالية. اختيار الاستراتيجية الصحيحة من البداية يُجنّبك عمليات نقل مؤلمة للمخطط في وقت لاحق.

لمحة سريعة عن الاستراتيجيات الثلاث

  • SINGLE_TABLE — جدول واحد للتسلسل الهرمي بأكمله، يُحدّد عمود المميّز النوع الفرعي.
  • JOINED — جدول واحد لكل فئة؛ تشترك جداول الأنواع الفرعية في المفتاح الأساسي ذاته وتنضم إلى الوالد.
  • TABLE_PER_CLASS — جدول مستقل لكل فئة ملموسة؛ لا أعمدة مشتركة ولا انضمامات.
الاستراتيجية الافتراضية: إذا علّمت فئة بـ@Inheritance دون تحديد strategy، تتخذ JPA SINGLE_TABLE افتراضيًا. معرفة هذا تمنع قرارات مخطط غير مقصودة.

الاستراتيجية الأولى — SINGLE_TABLE

تُعيَّن جميع فئات التسلسل الهرمي على جدول واحد. تضيف تعليقة @DiscriminatorColumn عمودًا تُخبر قيمتُه Hibernate أيَّ نوع فرعي يمثّله الصف المعني. تُخزَّن أعمدة النوع الفرعي التي لا تنطبق على صف بعينه كـNULL.

import jakarta.persistence.*; @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "payment_type", discriminatorType = DiscriminatorType.STRING) @Table(name = "payments") public abstract class Payment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private java.math.BigDecimal amount; private java.time.LocalDateTime createdAt; // getters / setters محذوفة للإيجاز } @Entity @DiscriminatorValue("CREDIT_CARD") public class CreditCardPayment extends Payment { private String maskedCardNumber; // مثال: "****-****-****-4242" private String cardholderName; } @Entity @DiscriminatorValue("BANK_TRANSFER") public class BankTransferPayment extends Payment { private String iban; private String bankCode; }

ينتج DDL المُولَّد لهذا التسلسل الهرمي جدول payments واحدًا يشمل أعمدة الأنواع الثلاثة جميعها بالإضافة إلى عمود المميّز payment_type. يحمل صف CreditCardPayment قيمة NULL في iban وbank_code؛ ويحمل صف BankTransferPayment قيمة NULL في masked_card_number وcardholder_name.

SINGLE_TABLE هي الأسرع للاستعلامات متعددة الأشكال. يستلزم جلب جميع كيانات Payment جملة SELECT واحدة فقط بلا انضمامات. وهي أفضل خيار افتراضي حين يكون التسلسل الهرمي ضحلًا وأعمدة النوع الفرعي قليلة أو تقبل القيم الخالية بطبيعتها. طبّق قيود NOT NULL على مستوى التطبيق (عبر @NotNull في Bean Validation) بدلًا من مستوى قاعدة البيانات حين تختار هذه الاستراتيجية.
احذر الجداول العريضة الشحيحة. إذا كان التسلسل الهرمي يضم أنواعًا فرعية كثيرة يمتلك كل منها أعمدة خاصة به، يتحوّل مخطط SINGLE_TABLE إلى جدول عريض مليء بالقيم الخالية. يتعامل محسّن قاعدة البيانات مع هذا بكفاءة أقل ويصبح المخطط عسير الفهم. انتقل إلى JOINED في تلك الحالة.

الاستراتيجية الثانية — JOINED

تحصل كل فئة في التسلسل الهرمي على جدولها الخاص. يحتفظ الجدول الوالد بالأعمدة المشتركة؛ يحتوي جدول كل نوع فرعي فقط على الأعمدة الفريدة لذلك النوع ويشترك في قيمة المفتاح الأساسي ذاتها مع الصف الوالد. يُنشئ Hibernate انضمام JOIN كلما احتاج إلى إنشاء كائن نوع فرعي كامل.

@Entity @Inheritance(strategy = InheritanceType.JOINED) @Table(name = "payments") public abstract class Payment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private java.math.BigDecimal amount; private java.time.LocalDateTime createdAt; } @Entity @Table(name = "credit_card_payments") public class CreditCardPayment extends Payment { private String maskedCardNumber; private String cardholderName; } @Entity @Table(name = "bank_transfer_payments") public class BankTransferPayment extends Payment { private String iban; private String bankCode; }

يُولّد تحميل CreditCardPayment عبر المعرّف:

SELECT p.id, p.amount, p.created_at, c.masked_card_number, c.cardholder_name FROM payments p INNER JOIN credit_card_payments c ON c.id = p.id WHERE p.id = ?

يستلزم الاستعلام متعدد الأشكال — SELECT p FROM Payment p — انضمام LEFT OUTER JOIN عبر كل جدول نوع فرعي. مع ثلاثة أنواع فرعية يُنشئ Hibernate ثلاثة انضمامات. هذا صحيح لكنه يزداد تكلفةً كلما نما التسلسل الهرمي.

تمنحك JOINED مخططًا منظَّمًا. أعمدة الأنواع الفرعية غير قابلة للقيم الخالية على مستوى قاعدة البيانات (إذ تقع في جدولها الخاص)، وتُطبَّق نزاهة المفتاح الأجنبي بصورة طبيعية، كما أن استعلامات التقارير على نوع فرعي واحد سريعة. ادفع تكلفة الانضمام فقط عند التحميل متعدد الأشكال.

أعمدة المميّز مع JOINED

لا تشترط JOINED افتراضيًا وجود عمود مميّز — يمكن لـHibernate استنتاج النوع الفرعي من جداول الانضمام الموجودة. ومع ذلك، يُنصح بإضافته من أجل وضوح القراءة وللأدوات التي تستعلم عن قاعدة البيانات مباشرةً:

@Entity @Inheritance(strategy = InheritanceType.JOINED) @DiscriminatorColumn(name = "payment_type") @Table(name = "payments") public abstract class Payment { ... }

الاستراتيجية الثالثة — TABLE_PER_CLASS

تمتلك كل فئة ملموسة جدولًا مستقلًا تمامًا يُعيد تكرار أعمدة الوالد. لا يوجد جدول والد مشترك ولا انضمام بين الجداول المتجاورة.

@Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public abstract class Payment { @Id @GeneratedValue(strategy = GenerationType.TABLE) // IDENTITY غير مسموح به private Long id; private java.math.BigDecimal amount; private java.time.LocalDateTime createdAt; } @Entity @Table(name = "credit_card_payments") public class CreditCardPayment extends Payment { private String maskedCardNumber; private String cardholderName; } @Entity @Table(name = "bank_transfer_payments") public class BankTransferPayment extends Payment { private String iban; private String bankCode; }

لاحظ استخدام GenerationType.TABLE بدلًا من IDENTITY. لعدم وجود جدول والد مشترك، لا تستطيع أعمدة الهوية على مستوى قاعدة البيانات ضمان التفرد عبر جميع جداول الأنواع الفرعية. يُشترط استخدام تسلسل جدول JPA أو مفتاح أساسي من نوع UUID.

يُجبر الاستعلام متعدد الأشكال — SELECT p FROM Payment p — Hibernate على إصدار UNION ALL عبر جميع الجداول الملموسة:

SELECT id, amount, created_at, masked_card_number, cardholder_name, NULL AS iban, NULL AS bank_code FROM credit_card_payments UNION ALL SELECT id, amount, created_at, NULL, NULL, iban, bank_code FROM bank_transfer_payments
TABLE_PER_CLASS هي الاستراتيجية الأقل توصيةً في معظم حالات الاستخدام. تتحول الاستعلامات متعددة الأشكال إلى عمليات UNION ALL لا تستطيع استخدام الفهارس بفاعلية على الجداول الكبيرة. استخدمها فقط حين لا تحتاج أبدًا إلى الاستعلام عن التسلسل الهرمي بصورة متعددة الأشكال — مثلًا حين تُستعلم الأنواع الفرعية دائمًا مباشرةً عبر نوعها الملموس والتسلسل الهرمي ثابت.

اختيار الاستراتيجية — دليل القرار

  • تسلسل هرمي ضحل، أنواع فرعية قليلة، قبول الحقول القابلة للقيم الخاليةSINGLE_TABLE. أعلى أداء للقراءة، أبسط مخطط.
  • تسلسل هرمي عميق، نزاهة قوية في قاعدة البيانات مطلوبة، قيود NOT NULL مهمةJOINED. منظَّم، مرن، يدفع تكلفة الانضمام عند التحميل متعدد الأشكال.
  • الأنواع الفرعية تُستعلم دائمًا باستقلالية، لا حاجة لـ JPQL متعدد الأشكالTABLE_PER_CLASS. تجنّبها إلا إذا كنت متيقّنًا من نمط الاستعلام.

الاستعلامات متعددة الأشكال وطبقة المستودع

يعمل Spring Data JPA بسلاسة مع الاستراتيجيات الثلاث. صرّح بمستودع أساسي على النوع الوالد للاستعلام عن التسلسل الهرمي بأكمله، ومستودعات ملموسة لمعرِّفات النوع الفرعي الخاصة:

// يستعلم عن التسلسل الهرمي الكامل — SELECT واحد (SINGLE_TABLE) أو UNION ALL (TABLE_PER_CLASS) public interface PaymentRepository extends JpaRepository<Payment, Long> { List<Payment> findByAmountGreaterThan(java.math.BigDecimal threshold); } // يستعلم فقط عن صفوف بطاقة الائتمان — لا UNION ولا انضمامات إضافية public interface CreditCardPaymentRepository extends JpaRepository<CreditCardPayment, Long> { List<CreditCardPayment> findByCardholderName(String name); }

الخلاصة

توفّر JPA ثلاث استراتيجيات لتعيين الوراثة لجسر التسلسلات الهرمية الكائنية إلى الجداول العلائقية. تخزّن SINGLE_TABLE كل شيء في جدول واحد مع مميّز — استعلامات أسرع لكن الأعمدة قابلة للقيم الخالية. تُنظّم JOINED كل فئة في جدولها الخاص — أفضل ضمانات للنزاهة، وتدفع تكلفة الانضمام لكل تحميل متعدد الأشكال. تُكرّر TABLE_PER_CLASS أعمدة الوالد في كل جدول ملموس — تجنّب الاستعلامات متعددة الأشكال وتستلزم مُولِّد مفتاح من غير نوع IDENTITY. اختر الاستراتيجية التي تتوافق مع متطلبات نزاهة مخططك ونمط استعلامك السائد، وقاوم تغييرها لاحقًا — فالانتقال بين الاستراتيجيات يستلزم تغييرات DDL على بيانات الإنتاج.