علاقات الكيانات والارتباطات

نمذجة العلاقات

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

نمذجة العلاقات

تُمثّل قواعد البيانات العلائقية العالمَ على شكل جداول مترابطة بمفاتيح خارجية، في حين يُمثّله الكود الكائني التوجّه على شكل كائنات مترابطة بمراجع. مهمة 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 بيانات قديمة.

الاتجاهية مفهوم يخصّ Java وليس SQL. تُخزّن قاعدة البيانات دائمًا عمود مفتاح خارجي واحدًا بالضبط بصرف النظر عن كونك قد عيّنت الارتباط ثنائيًا أم لا. إضافة الجانب العكسي إلى فئة Java لا تكلّف شيئًا في المخطط — إنّها تُضيف فقط مرجعًا قابلًا للتنقّل في الرسم البياني للكائنات.

نطاق عمل ملموس للتطبيق

المثال الأساسي طوال هذا البرنامج التعليمي هو نطاق تجارة إلكترونية يضمّ أربعة كيانات: Customer وOrder وOrderItem وProduct. فيما يلي كيف تبدو العلاقات قبل تطبيق أي تعليقات توضيحية:

// تستخدم هذه الدروس استيرادات jakarta.persistence // (Spring Boot 3 يشحن مع Hibernate 6 الذي يستخدم jakarta.* لا javax.*) import jakarta.persistence.*; import java.util.List; @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; // الجانب ثنائي الاتجاه — Customer يعرف طلباته @OneToMany(mappedBy = "customer") private List<Order> orders; // getters / setters محذوفة للإيجاز }
@Entity @Table(name = "orders") public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // الجانب المالك للعلاقة Customer <-> Order @ManyToOne @JoinColumn(name = "customer_id") private Customer customer; // الجانب المالك للعلاقة Order <-> OrderItem @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) private List<OrderItem> items; }
@Entity @Table(name = "order_items") public class OrderItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "order_id") private Order order; @ManyToOne @JoinColumn(name = "product_id") private Product product; private int quantity; private java.math.BigDecimal unitPrice; }
@Entity @Table(name = "products") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private java.math.BigDecimal price; // أحادي الاتجاه — Product لا يعرف كائنات OrderItem التي تُشير إليه // (لا يوجد حقل هنا يُشير إلى OrderItem) }

لاحظ أن Product لا يحمل مرجعًا للعودة إلى OrderItem. هذا قرار تصميمي متعمّد لجعل الارتباط أحادي الاتجاه: كتالوج المنتجات ليس لديه أي سبب تجاري لتكرار كل سطر طلب أشار إليه على الإطلاق. إبقاؤه أحادي الاتجاه يبسّط الفئة ويتجنّب تحميل مجموعة ضخمة محتملة إلى الذاكرة.

الجانب المالك ولماذا يهمّ

في الارتباط ثنائي الاتجاه، تطلب JPA منك تعيين أحد الجانبين بوصفه الجانب المالك. الجانب المالك هو الذي ينظر إليه Hibernate لتحديد ما يكتبه في عمود المفتاح الخارجي. أما الجانب الآخر فيُسمّى الجانب العكسي ويُوسَم بـ mappedBy.

القاعدة بسيطة: الجانب الذي يحمل @JoinColumn هو الجانب المالك. في زوج @ManyToOne / @OneToMany، يمتلك طرف @ManyToOne العلاقة دائمًا لأن ذلك هو الجدول الذي يحمل عمود المفتاح الخارجي فعليًا.

إن قمت بتحديث الجانب العكسي فقط، لن يحفظ Hibernate التغيير. افترض أنك أضفت Order إلى customer.getOrders() لكنك نسيت ضبط order.setCustomer(customer). يقرأ Hibernate الجانب المالك (order.customer)، يجد null، ويكتب NULL في عمود customer_id — أو يرمي انتهاك قيد. احرص دائمًا على ضبط كلا جانبَي العلاقة ثنائية الاتجاه.

دالة مساعدة لكلا الجانبين

الأسلوب القياسي في Java هو توفير دالة مساعدة في أحد الكيانات تضبط الطرفَين دفعةً واحدة، مما يُزيل خطر نسيان الجانب الآخر:

// داخل فئة Order public void addItem(OrderItem item) { items.add(item); // تحديث الجانب العكسي (المجموعة) item.setOrder(this); // تحديث الجانب المالك } public void removeItem(OrderItem item) { items.remove(item); item.setOrder(null); }

يكتب المستدعون ببساطة order.addItem(item) ويبقى كلا الجانبين متسقَين دون أن يضطر المستدعي للتفكير في ذلك.

اختيار الاتجاهية في الممارسة العملية

قاعدة إرشادية مفيدة: ابدأ بـأدنى قدر من التنقّل تحتاجه حالات استخدامك فعليًا. الارتباطات ثنائية الاتجاه قوية لكنها تُدخل تقارنًا (coupling) — فالتغييرات في دورة حياة كيان قد تؤثر عرضًا على الآخر. تساعد الإرشادات التالية:

  • إن كنت تُحمّل الابن دائمًا انطلاقًا من الأب (مثلًا تحميل سطور الطلب)، يكفي @OneToMany أحادي الاتجاه من الأب إلى الأبناء.
  • إن كانت الاستعلامات تبدأ كثيرًا من الابن وتحتاج الأب (مثلًا تحميل سطر طلب والحاجة إلى بيانات الطلب)، اجعله ثنائي الاتجاه حتى يتمكّن Hibernate من الوصل (join) بكفاءة.
  • في @ManyToMany افضّل دائمًا الثنائية حتى يمكن الاستعلام من كلا جانبَي جدول الوصل دون SQL خام.
عيّن ما يتطلبه النطاق لا ما يبدو متماثلًا. يغري الإنسانَ إضافةُ مجموعة عكسية لكل كيان "احتياطًا"، لكن مجموعة @OneToMany غير ضرورية يُحمّلها Hibernate (كسولًا أو متحمّسًا) وتُضيف عبئًا. أضف الجانب العكسي فقط حين يتنقّل تطبيقك فعليًا في ذلك الاتجاه.

الخلاصة

تتحدّد العلاقات في JPA بقرارَين مستقلَّين: تعدّد الارتباط (@OneToOne و@OneToMany و@ManyToOne و@ManyToMany) والاتجاهية (أحادية أو ثنائية). الجانب المالك هو الكيان الذي يحمل عمود المفتاح الخارجي وهو الجانب الوحيد الذي يستشيره Hibernate عند الكتابة إلى قاعدة البيانات. في الارتباطات ثنائية الاتجاه، أنت المسؤول عن إبقاء كلا المرجعَين في Java متزامنَين — والدوال المساعدة في الكيان هي الأسلوب القياسي لإنفاذ ذلك. في الدروس التالية ستُطبّق كل نوع ارتباط بالتفصيل، بدءًا من @OneToOne.