انتقل إلى المحتوى الرئيسي

الفصل 2: العلاقات بين الكيانات

مقدمة​

في العالم الحقيقي، البيانات مرتبطة:

  • عميل يقوم بعدة طلبات
  • قسم يحتوي على عدة موظفين
  • طالب يتابع عدة دورات

هذا الفصل يعلمك كيفية نمذجة هذه العلاقات باستخدام JPA!

ما ستتعلمه​

  1. الأنواع الثلاثة من العلاقات (@OneToOne, @OneToMany/@ManyToOne, @ManyToMany)
  2. أنواع الجلب (LAZY مقابل EAGER)
  3. عمليات الكاسكيد
  4. العلاقات ثنائية الاتجاه

المدة: 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@OneToOneUser ↔ Profileلا
N-1@ManyToOneEmployee → Departmentلا
1-N@OneToManyDepartment → Employeesلا
N-N@ManyToManyStudent ↔ Courseنعم

أنواع الجلب​

EAGER: تحميل فوري (JOIN)
LAZY: تحميل عند الطلب (موصى به)

الكاسكيد​

PERSIST: حفظ في الكاسكيد
REMOVE: حذف في الكاسكيد
ALL: جميع العمليات

التقييم الذاتي​

  1. ما التعليق المناسب لـ "قسم لديه عدة موظفين"؟
  2. ما الفرق بين LAZY و EAGER؟
  3. ماذا يفعل orphanRemoval = true؟
  4. لماذا نزامن العلاقات ثنائية الاتجاه؟

الإجابات:

  1. @OneToMany على Department، @ManyToOne على Employee
  2. LAZY يحمل عند الطلب، EAGER يحمل فورًا
  3. يحذف الكيانات التي تمت إزالتها من المجموعة
  4. لكي تبقى الجانبان من العلاقة متسقين

الخطوة التالية​

أنت تتقن العلاقات! انتقل إلى الاستعلامات المخصصة باستخدام @Query.

➡️ الفصل 3: @Query

⬅️ الفصل السابق: الاتفاقيات

⬅️ العودة إلى الوحدة 6