@OneToOne
@OneToOne
ارتباط "واحد بواحد" هو أبسط العلاقات في النموذج العلائقي: يقابل صفٌّ واحد في الجدول A صفًّا واحدًا بالضبط في الجدول B. في JPA / Hibernate تُعيّن التعليمة @OneToOne هذه العلاقة إلى كائنات Java. معرفة أين يقع المفتاح الأجنبي وأيّ الطرفين يمتلكه تُحدّد كيفية توليد DDL وآلية الإدراج وطريقة تحميل الارتباط، لذلك سنستعرض كل متغير ذي معنى بالتفصيل.
النطاق التطبيقي: User و UserProfile
مثال كلاسيكي هو كيان User مع كيان UserProfile مقابل له يحمل البيانات الموسّعة. لكل مستخدم ملف شخصي واحد على الأكثر، وكل ملف شخصي ينتمي إلى مستخدم واحد بالضبط.
المتغير الأول — المفتاح الأجنبي على الطرف المالك
يخزّن التخطيط الأكثر شيوعًا المفتاح الأجنبي في جدول الطفل (user_profiles). يُسمّى الكيان الذي يحمل عمود المفتاح الأجنبي فعليًا الطرف المالك. أما الكيان الآخر فيُسمّى الطرف العكسي (تناولناه بالتفصيل في الدرس الخامس).
يولّد Hibernate عمود user_id مع قيد UNIQUE في جدول user_profiles. سمة unique = true في @JoinColumn هي ما يُخبر قاعدة البيانات بفرض عدد "واحد على الأكثر" — بدونها تصبح العلاقة many-to-one على مستوى قاعدة البيانات.
User، تُخبر mappedBy = "user" Hibernate قائلةً: "عمود المفتاح الأجنبي الحقيقي موجود في UserProfile.user". طرف واحد فقط يمكنه امتلاك الارتباط؛ ونسيان هذا يجعل Hibernate يُنشئ عمودَي مفتاح أجنبي — أحدهما لكل كيان — وهو خطأ في الغالب.
المتغير الثاني — المفتاح الأساسي المشترك
بديل آخر يستخدم قيمة المفتاح الأساسي ذاتها في الجدولين. يُعيد صف الطفل استخدام PK الأب بوصفه PK الخاص به. هذا كفء (لا حاجة لفهرس FK إضافي) وواضح دلاليًا حين لا يمكن للطفل أن يوجد بصورة مستقلة. استخدم @MapsId لتوصيل هذا:
حفظ ارتباط OneToOne
نظرًا لأن User يمتلك سياسة الـ cascade (CascadeType.ALL)، يحفظ حفظُ الأب الطفلَ تلقائيًا:
profile.setUser(user) يُبقي عمود FK بقيمة NULL. ضبط الطرف المالك وحده كافٍ للحفظ، لكن ضبط الطرفين يُبقي الـ cache من المستوى الأول متسقًا ويمنع NullPointerException المُربكة عند استعراض user.getProfile() داخل نفس المعاملة.
خيارات نوع التحميل
القيمة الافتراضية في JPA لـ @OneToOne هي EAGER، ما يعني أن كل مرة تُحمّل فيها كيان User، يُجري Hibernate فورًا join ليُحمّل UserProfile. في معظم التطبيقات يُعدّ هذا مبذّرًا حين لا تحتاج إلى الملف الشخصي.
- EAGER (الافتراضي) — يُضمَّن في نفس SELECT. بسيط، لكنه يُحمّل بيانات قد لا تحتاجها.
- LAZY — يُحمَّل الملف الشخصي فقط عند استدعاء
user.getProfile(). الخيار الصحيح تقريبًا دائمًا؛ يتطلب أن تكون الجلسة مفتوحة عند الوصول.
لتفعيل LAZY على الطرف العكسي يحتاج Hibernate إلى توليد فئة بروكسي فرعية. يعمل هذا بشكل موثوق مع تحسين كود البايت (مُفعَّل افتراضيًا في Spring Boot 3 عبر Hibernate Enhancer) أو مع تلميح @LazyToOne(LazyToOneOption.NO_PROXY) في الإعدادات الأقدم.
القراءة عبر Spring Data JPA
تبدو الـ repository كأي repository آخر:
نظرًا لأن الملف الشخصي LAZY، يعمل الوصول إليه داخل method خدمة @Transactional بسلاسة. أما الوصول خارج المعاملة (في controller بعد إغلاق الجلسة) فيُلقي LazyInitializationException — الحل الاعتيادي هو تحميل ما تحتاجه داخل الخدمة أو استخدام DTO projection.
الخلاصة
يُعيّن ارتباط @OneToOne كيانَين يتشاركان العلاقة الواحدة بواحدة. الطرف المالك يحمل @JoinColumn (وعمود FK الفعلي)؛ أما الطرف العكسي فيستخدم mappedBy. لتخطيط المفتاح المشترك استخدم @MapsId. أعلن دائمًا fetch = LAZY لتجنّب تحميل بيانات غير ضرورية، واضبط كلا طرفي العلاقة في الذاكرة عند الحفظ، وأضف unique = true إلى @JoinColumn لتفرض قاعدة البيانات قيد الأعداد. الدرس التالي يوسّع هذا المفهوم إلى علاقتَي one-to-many و many-to-one الأكثر شيوعًا.