الفصل 2: العلاقات بين الكيانات
مقدمة
في العالم الحقيقي، البيانات مرتبطة:
- عميل يقوم بعدة طلبات
- قسم يحتوي على عدة موظفين
- طالب يتابع عدة دورات
هذا الفصل يعلمك كيفية نمذجة هذه العلاقات باستخدام JPA!
ما ستتعلمه
- الأنواع الثلاثة من العلاقات (@OneToOne, @OneToMany/@ManyToOne, @ManyToMany)
- أنواع الجلب (LAZY مقابل EAGER)
- عمليات الكاسكيد
- العلاقات ثنائية الاتجاه
المدة: 1 ساعة و30 دقيقة
مشكلة بدون علاقات
نهج ساذج (سيء)
@Entity
public class Employee {
@Id
private Long id;
private String name;
private Long departmentId; // ❌ مجرد معرف!
}
@Entity
public class Department {
@Id
private Long id;
private String name;
}
مشاكل:
// لاسترجاع القسم لموظف:
Employee emp = empRepo.findById(1L);
Department dept = deptRepo.findById(emp.getDepartmentId()); // 2 استعلامات!
// لاسترجاع جميع الموظفين في قسم:
List<Employee> emps = empRepo.findByDepartmentId(deptId); // لا توجد علاقة!
الحل: علاقات JPA
مع العلاقات (جيد)
@Entity
public class Employee {
@Id
private Long id;
private String name;
@ManyToOne // ✅ علاقة JPA!
private Department department;
}
@Entity
public class Department {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "department") // ✅ علاقة عكسية!
private List<Employee> employees;
}
فوائد:
// قسم موظف - سهل!
Employee emp = empRepo.findById(1L);
Department dept = emp.getDepartment(); // وصول مباشر!
// موظفو قسم - سهل!
Department dept = deptRepo.findById(1L);
List<Employee> emps = dept.getEmployees(); // وصول مباشر!
النوع 1: @OneToOne (1-1)
ما هو؟
كيان مرتبط بكيان آخر واحد فقط.
أمثلة:
- User ↔ UserProfile (مستخدم لديه ملف شخصي)
- Person ↔ Passport (شخص لديه جواز سفر)
- Employee ↔ ParkingSpot (موظف لديه مكان لركن السيارة)
الكود: User و UserProfile
كيان User:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
// علاقة OneToOne
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id") // مفتاح خارجي في جدول المستخدمين
private UserProfile profile;
// منشئون، getters، setters
}
كيان UserProfile:
@Entity
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String bio;
private String avatar;
// علاقة عكسية (اختيارية)
@OneToOne(mappedBy = "profile")
private User user;
// منشئون، getters، setters
}
الاستخدام
// إنشاء
User user = new User("john", "john@email.com");
UserProfile profile = new UserProfile("السيرة الذاتية لجون", "avatar.jpg");
user.setProfile(profile);
userRepo.save(user); // الملف الشخصي محفوظ تلقائيًا (كاسكيد)
// قراءة
User user = userRepo.findById(1L);
UserProfile profile = user.getProfile();
System.out.println(profile.getBio());
النوع 2: @ManyToOne و @OneToMany (N-1 / 1-N)
ما هو؟
عدة كيانات مرتبطة بكيان واحد.
أمثلة:
- Department ↔ Employees (قسم لديه عدة موظفين)
- Author ↔ Books (مؤلف يكتب عدة كتب)
- Category ↔ Products (فئة تحتوي على عدة منتجات)
الكود: Department و Employee
جانب Many (Employee):
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double salary;
// عدة موظفين ينتمون إلى قسم واحد
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id") // FK في جدول الموظفين
private Department department;
// منشئون
public Employee() {}
public Employee(String name, Double salary) {
this.name = name;
this.salary = salary;
}
// Getters، setters
}
جانب One (Department):
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// قسم واحد لديه عدة موظفين
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees = new ArrayList<>();
// منشئون
public Department() {}
public Department(String name) {
this.name = name;
}
// طرق مساعدة
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this); // ارتباط ثنائي الاتجاه
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null);
}
// Getters، setters
}
الاستخدام
// إنشاء قسم مع موظفين
Department it = new Department("IT");
it.addEmployee(new Employee("Alice", 50000.0));
it.addEmployee(new Employee("Bob", 60000.0));
deptRepo.save(it); // الموظفون محفوظون تلقائيًا
// قراءة الموظفين في قسم
Department dept = deptRepo.findById(1L);
List<Employee> employees = dept.getEmployees();
employees.forEach(emp -> System.out.println(emp.getName()));
// قراءة القسم لموظف
Employee emp = empRepo.findById(1L);
Department dept = emp.getDepartment();
System.out.println(dept.getName());
النوع 3: @ManyToMany (N-N)
ما هو؟
عدة كيانات مرتبطة بعدة كيانات.
أمثلة:
- Students ↔ Courses (طالب يتابع عدة دورات، دورة لديها عدة طلاب)
- Books ↔ Authors (كتاب لديه عدة مؤلفين، مؤلف يكتب عدة كتب)
- Products ↔ Tags (منتج لديه عدة علامات، علامة على عدة منتجات)
يتطلب جدول ربط!
الكود: Student و Course
كيان Student:
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// عدة طلاب يتابعون عدة دورات
@ManyToMany
@JoinTable(
name = "student_course", // جدول الربط
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses = new ArrayList<>();
// طرق مساعدة
public void enrollCourse(Course course) {
courses.add(course);
course.getStudents().add(this);
}
public void dropCourse(Course course) {
courses.remove(course);
course.getStudents().remove(this);
}
// منشئون، getters، setters
}
كيان Course:
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
// علاقة عكسية
@ManyToMany(mappedBy = "courses")
private List<Student> students = new ArrayList<>();
// منشئون، getters، setters
}
الجدول الناتج:
CREATE TABLE student_course (
student_id BIGINT,
course_id BIGINT,
PRIMARY KEY (student_id, course_id)
);
الاستخدام
// إنشاء
Student alice = new Student("Alice");
Course java = new Course("Java Programming");
Course spring = new Course("Spring Boot");
alice.enrollCourse(java);
alice.enrollCourse(spring);
studentRepo.save(alice);
// قراءة الدورات لطالب
Student student = studentRepo.findById(1L);
List<Course> courses = student.getCourses();
// قراءة الطلاب لدورة
Course course = courseRepo.findById(1L);
List<Student> students = course.getStudents();
أنواع الجلب: LAZY مقابل EAGER
المشكلة
متى يتم تحميل الكيانات المرتبطة؟
Employee emp = empRepo.findById(1L);
// السؤال: هل تم تحميل القسم الآن؟
EAGER (تحميل فوري)
@ManyToOne(fetch = FetchType.EAGER)
private Department department;
السلوك:
Employee emp = empRepo.findById(1L);
// القسم محمل بالفعل!
Department dept = emp.getDepartment(); // لا يوجد استعلام إضافي
SQL الناتج:
SELECT e.*, d.*
FROM employee e
LEFT JOIN department d ON e.department_id = d.id
WHERE e.id = 1;
فوائد:
- ✅ البيانات متاحة فورًا
- ✅ لا يوجد استعلام إضافي
عيوب:
- ❌ تحميل البيانات حتى لو لم تستخدم
- ❌ قد يكون بطيئًا إذا كان هناك الكثير من العلاقات
LAZY (تحميل مؤجل) - موصى به
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
السلوك:
Employee emp = empRepo.findById(1L);
// القسم لم يتم تحميله بعد (proxy)
Department dept = emp.getDepartment(); // استعلام الآن
SQL الناتج:
-- الاستعلام الأول
SELECT * FROM employee WHERE id = 1;
-- الاستعلام الثاني (فقط إذا تم الوصول إلى القسم)
SELECT * FROM department WHERE id = ?;
فوائد:
- ✅ تحميل فقط إذا لزم الأمر
- ✅ أداء عام أفضل
عيوب:
- ⚠️ استعلام إضافي إذا تم الوصول
- ⚠️ قد يسبب مشكلة N+1
متى تستخدم ماذا؟
// ✅ LAZY لـ @OneToMany (قائمة)
@OneToMany(fetch = FetchType.LAZY) // افتراضي
private List<Employee> employees;
// ✅ LAZY لـ @ManyToMany (قائمة)
@ManyToMany(fetch = FetchType.LAZY) // افتراضي
private List<Course> courses;
// ⚠️ LAZY لـ @ManyToOne (موصى به)
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
// ⚠️ EAGER فقط إذا كان يستخدم دائمًا
@ManyToOne(fetch = FetchType.EAGER)
private Department department; // فقط إذا كان ضروريًا دائمًا
القاعدة الذهبية: استخدم LAZY افتراضيًا!
عمليات الكاسكيد
المشكلة
ماذا يحدث عندما أحفظ/أحذف كيانًا مع علاقات؟
Department dept = new Department("IT");
dept.addEmployee(new Employee("Alice"));
deptRepo.save(dept);
// السؤال: هل تم حفظ الموظف أيضًا؟
أنواع الكاسكيد
public enum CascadeType {
PERSIST, // حفظ في الكاسكيد
MERGE, // تحديث في الكاسكيد
REMOVE, // حذف في الكاسكيد
REFRESH, // تحديث في الكاسكيد
DETACH, // فصل في الكاسكيد
ALL // جميع العمليات
}
Cascade.PERSIST (حفظ)
@OneToMany(mappedBy = "department", cascade = CascadeType.PERSIST)
private List<Employee> employees;
السلوك:
Department dept = new Department("IT");
dept.addEmployee(new Employee("Alice"));
deptRepo.save(dept);
// ✅ الموظف محفوظ تلقائيًا!
Cascade.REMOVE (حذف)
@OneToMany(mappedBy = "department", cascade = CascadeType.REMOVE)
private List<Employee> employees;
السلوك:
deptRepo.deleteById(1L);
// ⚠️ جميع الموظفين في القسم محذوفون!
تحذير: استخدم بحذر!
Cascade.ALL (كل شيء)
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees;
السلوك:
// حفظ، تحديث، حذف في الكاسكيد
deptRepo.save(dept); // الموظفون محفوظون
deptRepo.delete(dept); // الموظفون محذوفون
توصيات
// ✅ جيد - الوالد يمتلك الأطفال
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items; // عناصر الطلب تنتمي إلى الطلب
// ⚠️ انتباه - الكيانات المشتركة
@ManyToOne(cascade = CascadeType.PERSIST)
private Department department; // لا REMOVE!
// ❌ خطير
@ManyToMany(cascade = CascadeType.REMOVE)
private List<Course> courses; // سيحذف الدورات!
orphanRemoval
ما هو؟
حذف الكيانات التي لم تعد مشار إليها تلقائيًا.
@OneToMany(mappedBy = "department", orphanRemoval = true)
private List<Employee> employees;
السلوك:
Department dept = deptRepo.findById(1L);
dept.getEmployees().remove(0); // إزالة من القائمة
deptRepo.save(dept);
// ✅ الموظف محذوف من قاعدة البيانات!
بدون orphanRemoval:
// ❌ الموظف يبقى في قاعدة البيانات مع department_id = NULL
العلاقات ثنائية الاتجاه
ما هو؟
الكيانان يعرفان بعضهما البعض.
// الموظف يعرف قسمه
@ManyToOne
private Department department;
// القسم يعرف موظفيه
@OneToMany(mappedBy = "department")
private List<Employee> employees;
مزامنة الجانبين
طرق مساعدة موصى بها:
@Entity
public class Department {
@OneToMany(mappedBy = "department")
private List<Employee> employees = new ArrayList<>();
// ✅ مساعد للمزامنة
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this); // مزامنة!
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null); // مزامنة!
}
}
الاستخدام:
Department dept = new Department("IT");
Employee emp = new Employee("Alice");
// ✅ جيد - استخدم المساعد
dept.addEmployee(emp);
// ❌ سيء - نسيان المزامنة
dept.getEmployees().add(emp); // نسيان emp.setDepartment(dept)!
ملخص الفصل
الأنواع الثلاثة من العلاقات
| النوع | التعليق | المثال | جدول الربط؟ |
|---|---|---|---|
| 1-1 | @OneToOne | User ↔ Profile | لا |
| N-1 | @ManyToOne | Employee → Department | لا |
| 1-N | @OneToMany | Department → Employees | لا |
| N-N | @ManyToMany | Student ↔ Course | نعم |
أنواع الجلب
EAGER: تحميل فوري (JOIN)
LAZY: تحميل عند الطلب (موصى به)
الكاسكيد
PERSIST: حفظ في الكاسكيد
REMOVE: حذف في الكاسكيد
ALL: جميع العمليات
التقييم الذاتي
- ما التعليق المناسب لـ "قسم لديه عدة موظفين"؟
- ما الفرق بين LAZY و EAGER؟
- ماذا يفعل
orphanRemoval = true؟ - لماذا نزامن العلاقات ثنائية الاتجاه؟
الإجابات:
@OneToManyعلى Department،@ManyToOneعلى Employee- LAZY يحمل عند الطلب، EAGER يحمل فورًا
- يحذف الكيانات التي تمت إزالتها من المجموعة
- لكي تبقى الجانبان من العلاقة متسقين
الخطوة التالية
أنت تتقن العلاقات! انتقل إلى الاستعلامات المخصصة باستخدام @Query.