نمذجة العلاقات
نمذجة العلاقات
تُمثّل قواعد البيانات العلائقية العالمَ على شكل جداول مترابطة بمفاتيح خارجية، في حين يُمثّله الكود الكائني التوجّه على شكل كائنات مترابطة بمراجع. مهمة Hibernate — وJPA بوجه أعم — هي التعيين (mapping) التلقائي بين هذين العالمين. قبل أن تلمس أي تعليق توضيحي (annotation) واحد، عليك أن تكون واضحًا بشأن أنواع الارتباطات الموجودة وعدد الصفوف المشاركة من كل جانب والاتجاه الذي يحتاج كودك إلى التنقّل فيه. إنّ امتلاك هذا النموذج الذهني الصحيح يجنّبك أشيع أخطاء التعيين التي يقع فيها المطوّرون حين يتعاملون مع مواصفة JPA لأوّل مرة.
أنواع تعدّد الارتباطات الأربعة
تُعرّف JPA أربعة تعليقات توضيحية تصف عدد الحالات التي يرتبط بها كيان واحد بكيان آخر. كل منها يُعيَّن مباشرةً على نمط مفتاح خارجي في قاعدة البيانات.
- @OneToOne — صفٌّ واحد في الجدول أ يقابل صفًّا واحدًا بالضبط في الجدول ب. مثال:
UserلهUserProfileواحد. - @OneToMany / @ManyToOne — صفٌّ واحد في أ يقابل صفوفًا كثيرة في ب. مثال:
Customerواحد له عديد من كائناتOrder. هذا النمط هو الأشيع في تطبيقات الأعمال بلا منازع. - @ManyToMany — صفوف كثيرة في أ تقابل صفوفًا كثيرة في ب مع الحاجة إلى جدول وصل (join table). مثال:
Studentيمكنه التسجيل في كائناتCourseمتعددة وكل مساق له طلاب كثيرون.
اختيار تعدّد غير مناسب يُعدّ خطأً يجتاز مرحلة الترجمة لكنه يكسر التنفيذ. احرص دائمًا على تحديد العلاقة من البيانات أوّلًا ثم اختر التعليق التوضيحي.
الاتجاهية: أحادية الاتجاه مقابل ثنائية الاتجاه
يخبرك تعدّد الارتباطات بالكمية؛ أما الاتجاهية فتخبرك من أيّ جانب يمكنك التنقّل في كود Java.
في الارتباط أحادي الاتجاه، تحمل فئة كيان واحدة فقط مرجعًا إلى الأخرى. يحمل Order مرجعًا إلى Customer الخاص به، لكن Customer لا يملك مجموعة من الطلبات. لا تزال قاعدة البيانات تحتوي على مفتاح خارجي، لكن Hibernate لن يسمح لك بكتابة customer.getOrders() لأن هذا الحقل غير موجود في فئة Java.
في الارتباط ثنائي الاتجاه، يحمل كلا الجانبين مراجع. يمتلك Order حقل Customer ويمتلك Customer حقل List<Order>. يمكنك التنقّل من أيّ جانب. هذا مريح للاستعلامات لكنّه يُدخل مسؤوليةً جديدة: يجب إبقاء كلا الجانبين متزامنَين في الذاكرة وإلا سيحفظ Hibernate بيانات قديمة.
نطاق عمل ملموس للتطبيق
المثال الأساسي طوال هذا البرنامج التعليمي هو نطاق تجارة إلكترونية يضمّ أربعة كيانات: Customer وOrder وOrderItem وProduct. فيما يلي كيف تبدو العلاقات قبل تطبيق أي تعليقات توضيحية:
لاحظ أن Product لا يحمل مرجعًا للعودة إلى OrderItem. هذا قرار تصميمي متعمّد لجعل الارتباط أحادي الاتجاه: كتالوج المنتجات ليس لديه أي سبب تجاري لتكرار كل سطر طلب أشار إليه على الإطلاق. إبقاؤه أحادي الاتجاه يبسّط الفئة ويتجنّب تحميل مجموعة ضخمة محتملة إلى الذاكرة.
الجانب المالك ولماذا يهمّ
في الارتباط ثنائي الاتجاه، تطلب JPA منك تعيين أحد الجانبين بوصفه الجانب المالك. الجانب المالك هو الذي ينظر إليه Hibernate لتحديد ما يكتبه في عمود المفتاح الخارجي. أما الجانب الآخر فيُسمّى الجانب العكسي ويُوسَم بـ mappedBy.
القاعدة بسيطة: الجانب الذي يحمل @JoinColumn هو الجانب المالك. في زوج @ManyToOne / @OneToMany، يمتلك طرف @ManyToOne العلاقة دائمًا لأن ذلك هو الجدول الذي يحمل عمود المفتاح الخارجي فعليًا.
Order إلى customer.getOrders() لكنك نسيت ضبط order.setCustomer(customer). يقرأ Hibernate الجانب المالك (order.customer)، يجد null، ويكتب NULL في عمود customer_id — أو يرمي انتهاك قيد. احرص دائمًا على ضبط كلا جانبَي العلاقة ثنائية الاتجاه.
دالة مساعدة لكلا الجانبين
الأسلوب القياسي في Java هو توفير دالة مساعدة في أحد الكيانات تضبط الطرفَين دفعةً واحدة، مما يُزيل خطر نسيان الجانب الآخر:
يكتب المستدعون ببساطة order.addItem(item) ويبقى كلا الجانبين متسقَين دون أن يضطر المستدعي للتفكير في ذلك.
اختيار الاتجاهية في الممارسة العملية
قاعدة إرشادية مفيدة: ابدأ بـأدنى قدر من التنقّل تحتاجه حالات استخدامك فعليًا. الارتباطات ثنائية الاتجاه قوية لكنها تُدخل تقارنًا (coupling) — فالتغييرات في دورة حياة كيان قد تؤثر عرضًا على الآخر. تساعد الإرشادات التالية:
- إن كنت تُحمّل الابن دائمًا انطلاقًا من الأب (مثلًا تحميل سطور الطلب)، يكفي
@OneToManyأحادي الاتجاه من الأب إلى الأبناء. - إن كانت الاستعلامات تبدأ كثيرًا من الابن وتحتاج الأب (مثلًا تحميل سطر طلب والحاجة إلى بيانات الطلب)، اجعله ثنائي الاتجاه حتى يتمكّن Hibernate من الوصل (join) بكفاءة.
- في
@ManyToManyافضّل دائمًا الثنائية حتى يمكن الاستعلام من كلا جانبَي جدول الوصل دون SQL خام.
@OneToMany غير ضرورية يُحمّلها Hibernate (كسولًا أو متحمّسًا) وتُضيف عبئًا. أضف الجانب العكسي فقط حين يتنقّل تطبيقك فعليًا في ذلك الاتجاه.
الخلاصة
تتحدّد العلاقات في JPA بقرارَين مستقلَّين: تعدّد الارتباط (@OneToOne و@OneToMany و@ManyToOne و@ManyToMany) والاتجاهية (أحادية أو ثنائية). الجانب المالك هو الكيان الذي يحمل عمود المفتاح الخارجي وهو الجانب الوحيد الذي يستشيره Hibernate عند الكتابة إلى قاعدة البيانات. في الارتباطات ثنائية الاتجاه، أنت المسؤول عن إبقاء كلا المرجعَين في Java متزامنَين — والدوال المساعدة في الكيان هي الأسلوب القياسي لإنفاذ ذلك. في الدروس التالية ستُطبّق كل نوع ارتباط بالتفصيل، بدءًا من @OneToOne.