الجانب المالك مقابل الجانب العكسي (mappedBy)
الجانب المالك مقابل الجانب العكسي (mappedBy)
عندما تُنمذج علاقةً ثنائية الاتجاه في JPA — مثلاً Customer يحمل قائمةً من كائنات Order، وكل Order يحمل مرجعًا عكسيًا إلى Customer الخاص به — يجب أن يعرف Hibernate أيّ الحقلين في Java يقابل عمود المفتاح الأجنبي الفعلي في قاعدة البيانات. هذا هو السؤال الوحيد الذي تُجيب عنه ثنائية الجانب المالك / الجانب العكسي، والخطأ فيها هو أحد أكثر الأسباب شيوعًا لتجاهل تغييرات الارتباطات بصمت عند وقت الإفراغ (flush).
القاعدة الأساسية
في كل علاقة ثنائية الاتجاه يوجد جانبان بالضبط:
- الجانب المالك — الحقل الذي يحمل عمود المفتاح الأجنبي الفعلي. يقرأ Hibernate هذا الحقل ليقرر ما يكتبه في قاعدة البيانات. يُزيَّن هذا الجانب بـ
@JoinColumn(لـ@ManyToOne/@OneToOne) أو@JoinTable(لـ@ManyToMany). - الجانب العكسي — الحقل "المرآة" على الكيان الآخر. يوجد فقط للتنقل في رسم كائنات Java. يُهمل Hibernate التغييرات التي تُجرى حصرًا على الجانب العكسي عند الكتابة إلى قاعدة البيانات. يُعلَن عن هذا الجانب بالخاصية
mappedBy.
mappedBy = "fieldName" تقول "المفتاح الأجنبي لهذه العلاقة تديره الحقل المسمى fieldName على الكيان الآخر — أنا مجرد مرآة للقراءة فحسب."
مثال ثنائي الاتجاه: @ManyToOne / @OneToMany
هذا هو أكثر أنواع العلاقات ثنائية الاتجاه شيوعًا. عميل (Customer) لديه طلبات عديدة (Order)؛ وكل طلب ينتمي إلى عميل واحد. يقع المفتاح الأجنبي (customer_id) في جدول orders، لذا فإن حقل Order.customer هو الجانب المالك.
ما يحدث إذا ضبطتَ الجانب العكسي فقط
هذا هو الفخ الكلاسيكي. يُضيف مطوّر طلبًا إلى قائمة العميل لكنه ينسى تعيين المرجع العكسي على الطلب:
لأن customer.orders هو الجانب العكسي (يحمل mappedBy)، فإن Hibernate ببساطة لا يفحصه عند توليد SQL. الحل دائمًا هو تعيين الجانبين معًا:
علاقة @OneToOne ثنائية الاتجاه
تنطبق القاعدة ذاتها. اختر كيانًا واحدًا ليحمل عمود FK وزيّن الآخر بـ mappedBy:
علاقة @ManyToMany ثنائية الاتجاه
في علاقة كثير-إلى-كثير، يقع المفتاح الأجنبي في جدول ربط. يحتاج كيان واحد فقط إلى تعريف جدول الربط بـ @JoinTable؛ بينما يستخدم الآخر mappedBy:
course.students وحدها لن تُدرج صفًا في جدول الربط student_course أبدًا، لأن course.students هو الجانب العكسي. أضف دائمًا المقرر إلى مجموعة الطالب (أو استخدم دالةً مساعدةً ثنائية الجانب) حتى يرى Hibernate التغيير على الجانب المالك.
اختيار الجانب الذي يملك المفتاح الأجنبي
في علاقة @ManyToOne / @OneToMany يكون الاختيار طبيعيًا: يقع عمود FK دائمًا على جانب "الكثير" في الجدول، لذا فإن حقل @ManyToOne في كيان "الكثير" هو دائمًا الجانب المالك. لا يمكنك عكس ذلك دون إعادة هيكلة المخطط.
في علاقات @OneToOne و@ManyToMany لديك مرونة أكبر. اتفاق شائع هو وضع الجانب المالك على الكيان الأكثر "تبعيةً" أو "أبناءً" — الذي لا يمكنه الوجود دون الآخر. بهذه الطريقة يقع المفتاح الأجنبي أو جدول الربط بشكل طبيعي بجانب البيانات التي تعتمد عليه.
مرجع سريع: قواعد mappedBy
- يستخدم
mappedByجانب واحد بالضبط في كل زوج ثنائي الاتجاه؛ بينما يملك الجانب الآخر المفتاح الأجنبي. - قيمة
mappedByهي اسم الحقل على الكيان المالك، وليس اسم العمود. - يجب أن لا يحمل الجانب الذي يستخدم
mappedByأيضًا@JoinColumnأو@JoinTable— فهذه تنتمي إلى الجانب المالك. - التغييرات التي تُجرى فقط على الجانب العكسي (
mappedBy) يتجاهلها Hibernate بصمت عند وقت الإفراغ. - زامن كلا الجانبين في الذاكرة دائمًا، حتى لو كان الجانب المالك فقط هو الذي يقود SQL — فالحالة القديمة في الذاكرة تُسبب أخطاءً صعبة التتبع في الذاكرة المخبئية ذات المستوى الثاني أو في عمليات القراءة ضمن المعاملة.
الخلاصة
الجانب المالك هو الحقل الذي يحمل عمود المفتاح الأجنبي (مُزيَّن بـ @JoinColumn أو @JoinTable). الجانب العكسي يُعلَن بـ mappedBy وهو مجرد وسيلة تنقل في Java — لا يكتب Hibernate شيئًا في قاعدة البيانات بناءً عليه. إن فهم هذا التمييز يمنع أكثر أخطاء JPA شيوعًا: تحديث الجانب العكسي فقط وعدم رؤية أي تغيير في قاعدة البيانات. استخدم دوالًا مساعدةً تُزامن كلا الجانبين معًا، ولن تعاني من هذا مرةً أخرى.