كائنات القيمة القابلة للتضمين و@Embedded
كائنات القيمة القابلة للتضمين و@Embedded
ليست كل كائنات نموذج المجال بحاجة إلى جدول قاعدة بيانات خاص بها. أحيانًا تُشكّل مجموعة من الحقول المترابطة — عنوان بريدي، أو مبلغ مالي، أو نطاق زمني — كائنَ قيمة متماسكًا مفهوميًا، وفي الوقت ذاته يكون تخزين كل أعمدته في جدول الكيان المالك أمرًا بالغ المنطقية. يُرسّخ JPA هذا النمط عبر تعليقَيْن توضيحيَّيْن: @Embeddable و@Embedded.
المشكلة: أعمدة مسطّحة ونموذج مجال غني
تأمّل كيانًا Customer يملك عنوان شحن. يوزّع التعيين السطحي حقول العنوان مباشرةً على الكيان:
هذا يُصرَّف ويعمل، لكنك تفقد مفهوم المجال لـالعنوان. لا يمكنك إعادة استخدام بنية العنوان في كيان Order، ولا إضافة التحقق الخاص بالعنوان في مكان واحد، ولا تمرير كائن Address في طبقة الخدمة. مخطط قاعدة البيانات متطابق في الحالتين — الهدف هو Java أكثر ثراءً دون تغيير المخطط.
@Embeddable و@Embedded
ضع تعليق @Embeddable على فئة عادية لتُخبر Hibernate بأنها كائن قيمة يجب إدراج حقوله مضمَّنةً في جدول المالك. ثم ضع تعليق @Embedded على الحقل في الكيان المالك.
يُعيّن Hibernate هذا إلى جدول customers واحد يحتوي على أعمدة id وname وstreet وcity وpostal_code وcountry. لا وصل (join)، ولا جدول إضافي، ولا مفتاح أجنبي — مجرد أعمدة مسطّحة.
@Embedded صراحةً ممارسةً جيدة لأنه يوصّل النية بوضوح للقرّاء الذين قد لا يتذكّرون أن Address تحمل التعليق.
تجاوز أسماء الأعمدة بـ@AttributeOverride
تأتي أسماء الأعمدة الافتراضية من أسماء الحقول في فئة @Embeddable. تنشأ مشكلات حين تضمّن النوع ذاته مرتين في كيان واحد — مثلًا Customer يملك عنوان شحن وعنوان فوترة معًا. كلاهما سيحاول إنشاء عمود باسم street، مما يُسبّب تعارضًا في التعيين.
حلّ التعارض باستخدام @AttributeOverride:
يحتوي الجدول الآن على ثمانية أعمدة متمايزة مسبوقة بـship_ وbill_، بينما تعمل الشيفرة البرمجية مع كائنَيْ Address ذوَيْ نوع واضح.
الكائنات المضمَّنة الفارغة (Null Embeddables)
حين تكون كل الأعمدة المنتمية إلى قيمة مضمَّنة NULL في قاعدة البيانات، يُعيد Hibernate null للحقل المضمَّن بأكمله افتراضيًا. قد ينتج عن ذلك NullPointerException أول مرة تستدعي فيها customer.getShippingAddress().getCity(). احمِ نفسك بفحص null، أو هيّئ الحقل بنسخة أولية في مُنشئ الكيان.
shippingAddress قبل حفظ سجل، فسيُعطيك Hibernate قيمة null عند تحميله — لا كائن Address فارغ. هيّئ دائمًا الحقول المضمَّنة بقيمة افتراضية آمنة في مُنشئ الكيان، أو تحقق من null في موقع الاستدعاء.
تداخل الكائنات المضمَّنة
يمكن لفئة @Embeddable أن تحتوي بدورها على @Embeddable أخرى. مثال حقيقي شائع هو Address تضمّن GeoPoint:
يُسطّح Hibernate جميع الأعمدة المتداخلة في الجدول ذاته. ابقِ التداخل ضحلًا — مستوى أو مستويان هو الحد العملي قبل أن تُصبح قراءة الشيفرة وإدارة تجاوزات الأعمدة مُجهِدَيْن.
الكائنات المضمَّنة كـقيم غير قابلة للتغيير: equals وhashCode
يجب أن يكون كائن القيمة غير قابل للتغيير ومُقارَنًا بقيمته لا بهويته. اجعل فئات @Embeddable غير قابلة للتغيير حيثما أمكن — وفّر getters فقط، وضع كل الحقول في المُنشئ، ونفّذ equals() وhashCode() بناءً على قيم الحقول:
equals()/hashCode() مبنيَّيْن على القيمة، ومُنشئ محمي بلا وسائط لـJPA. في Java 16 وما بعده يمكنك استخدام record حقيقي مُعلَّق بـ@Embeddable — يدعم Hibernate 6.2 وما بعده ذلك أصلًا، إذ يحلّ المُنشئ الأساسي (canonical constructor) محل مُنشئ الوسائط الكاملة ويستخدم JPA المُنشئ المُدمج للترطيب (hydration).
المفاضلات في الأداء
تُخزَّن القيم المضمَّنة في الصف ذاته مع مالكها، مما يعني:
- لا وصل (join) مطلوب — قراءة
CustomerتسترجعAddressدائمًا في نفسSELECT. لا خيار للتحميل الكسول ولا خطر N+1 للجزء المضمَّن. - لا تحميل جزئي — إذا احتجت فقط اسم العميل، تُجلَب أعمدة العنوان رغم ذلك. استخدم الإسقاطات (تعبيرات المُنشئ في JPQL أو Spring Data Projections) حين تحتاج فعلًا تجنّب تحميل كائنات مضمَّنة كبيرة.
- جداول عريضة — تضمين كائنات قيمة كثيرة في كيان واحد يُنتج جداول ذات أعمدة كثيرة. هذا عادةً مقبول؛ محرّكات العلاقات تتعامل مع الصفوف العريضة جيدًا. فكّر في التطبيع فقط حين تكون البيانات المضمَّنة اختيارية فعلًا وضخمة الحجم.
الخلاصة
استخدم @Embeddable و@Embedded لتقسيم مجموعة أعمدة مسطّحة إلى كائنات قيمة غنية وقابلة لإعادة الاستخدام دون تغيير مخطط قاعدة البيانات. طبّق @AttributeOverride كلّما ظهر النوع المضمَّن ذاته أكثر من مرة في كيان واحد. اجعل الفئات المضمَّنة غير قابلة للتغيير ونفّذ المساواة المبنية على القيمة. المفاضلة بسيطة: تكسب غنى النموذج وإمكانية إعادة الاستخدام بدون أي وصل إضافي، لكنك لا تستطيع التحميل الكسول لكائن مضمَّن ولا مشاركته عبر صفوف متعددة. هذه الخصائص تجعل الكائنات المضمَّنة الخيار الصحيح لأنواع العنوان والمبلغ المالي والنطاق الزمني والإحداثيات التي تنتمي طبيعيًا إلى مالكها.