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

TP6.2 - العلاقات بين الموظف والقسم

الأهداف​

في نهاية هذا التمرين، ستكون قادرًا على:

  1. ✅ إنشاء الكيانات مع العلاقات @ManyToOne و@OneToMany
  2. ✅ إدارة العلاقات ثنائية الاتجاه
  3. ✅ استخدام عمليات الكاسكيد
  4. ✅ تجنب مشكلة N+1 باستخدام JOIN FETCH

المدة المقدرة: 2 ساعات


المتطلبات الأساسية​

  • ✅ إكمال الفصل 2 من الوحدة 6
  • ✅ إكمال TP6.1
  • ✅ Maven 3.9+ وJava 17+

التحضير: مشروع جديد​

Spring Initializr​

التكوين:

  • Artifact: employee-department
  • Dependencies: Spring Web, Spring Data JPA, H2 Database

التمرين 1: إنشاء الكيانات​

الخطوة 1.1: كيان القسم​

الملف: model/Department.java

package com.example.relations.model;

import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "departments")
public class Department {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
private String location;

// ONE department has MANY employees
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Employee> employees = new ArrayList<>();

// Constructeurs
public Department() {}

public Department(String name, String location) {
this.name = name;
this.location = location;
}

// Helper methods pour synchroniser la relation
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this);
}

public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null);
}

// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }

public List<Employee> getEmployees() { return employees; }
public void setEmployees(List<Employee> employees) { this.employees = employees; }
}

الخطوة 1.2: كيان الموظف​

الملف: model/Employee.java

package com.example.relations.model;

import jakarta.persistence.*;

@Entity
@Table(name = "employees")
public class Employee {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
private String email;
private Double salary;

// MANY employees belong to ONE department
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;

// Constructeurs
public Employee() {}

public Employee(String name, String email, Double salary) {
this.name = name;
this.email = email;
this.salary = salary;
}

// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }

public Double getSalary() { return salary; }
public void setSalary(Double salary) { this.salary = salary; }

public Department getDepartment() { return department; }
public void setDepartment(Department department) { this.department = department; }
}

التمرين 2: إنشاء المستودعات​

الخطوة 2.1: DepartmentRepository​

الملف: repository/DepartmentRepository.java

package com.example.relations.repository;

import com.example.relations.model.Department;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface DepartmentRepository extends JpaRepository<Department, Long> {

// Méthode simple
List<Department> findByLocation(String location);

// Avec JOIN FETCH pour éviter N+1
@Query("SELECT d FROM Department d JOIN FETCH d.employees")
List<Department> findAllWithEmployees();
}

الخطوة 2.2: EmployeeRepository​

الملف: repository/EmployeeRepository.java

package com.example.relations.repository;

import com.example.relations.model.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

// Traverser la relation
List<Employee> findByDepartmentName(String deptName);

// Avec salaire
List<Employee> findBySalaryGreaterThan(Double minSalary);

// Département ET salaire
List<Employee> findByDepartmentNameAndSalaryGreaterThan(
String deptName,
Double minSalary
);

// Avec JOIN FETCH
@Query("SELECT e FROM Employee e JOIN FETCH e.department")
List<Employee> findAllWithDepartment();

// Requête personnalisée
@Query("SELECT e FROM Employee e WHERE e.department.location = :location")
List<Employee> findByDepartmentLocation(@Param("location") String location);
}

التمرين 3: تهيئة البيانات​

الملف: config/DataLoader.java

package com.example.relations.config;

import com.example.relations.model.Department;
import com.example.relations.model.Employee;
import com.example.relations.repository.DepartmentRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DataLoader {

@Bean
CommandLineRunner initDatabase(DepartmentRepository deptRepo) {
return args -> {
System.out.println("=== Initialisation ===");

// Département IT
Department it = new Department("IT", "Montreal");
it.addEmployee(new Employee("Alice", "alice@example.com", 75000.0));
it.addEmployee(new Employee("Bob", "bob@example.com", 65000.0));
it.addEmployee(new Employee("Charlie", "charlie@example.com", 80000.0));
deptRepo.save(it);

// Département HR
Department hr = new Department("HR", "Quebec");
hr.addEmployee(new Employee("David", "david@example.com", 55000.0));
hr.addEmployee(new Employee("Eve", "eve@example.com", 60000.0));
deptRepo.save(hr);

// Département Sales
Department sales = new Department("Sales", "Montreal");
sales.addEmployee(new Employee("Frank", "frank@example.com", 50000.0));
sales.addEmployee(new Employee("Grace", "grace@example.com", 52000.0));
deptRepo.save(sales);

System.out.println("=== " + deptRepo.count() + " départements créés ===");
};
}
}

التمرين 4: إنشاء المتحكمات​

متحكم الأقسام​

الملف: controller/DepartmentController.java

package com.example.relations.controller;

import com.example.relations.model.Department;
import com.example.relations.repository.DepartmentRepository;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/departments")
public class DepartmentController {

private final DepartmentRepository deptRepo;

public DepartmentController(DepartmentRepository deptRepo) {
this.deptRepo = deptRepo;
}

@GetMapping
public List<Department> getAll() {
return deptRepo.findAll();
}

@GetMapping("/{id}")
public Department getById(@PathVariable Long id) {
return deptRepo.findById(id).orElseThrow();
}

@GetMapping("/with-employees")
public List<Department> getAllWithEmployees() {
return deptRepo.findAllWithEmployees();
}

@GetMapping("/location/{location}")
public List<Department> byLocation(@PathVariable String location) {
return deptRepo.findByLocation(location);
}
}

متحكم الموظفين​

الملف: controller/EmployeeController.java

package com.example.relations.controller;

import com.example.relations.model.Employee;
import com.example.relations.repository.EmployeeRepository;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/employees")
public class EmployeeController {

private final EmployeeRepository empRepo;

public EmployeeController(EmployeeRepository empRepo) {
this.empRepo = empRepo;
}

@GetMapping
public List<Employee> getAll() {
return empRepo.findAll();
}

@GetMapping("/department/{deptName}")
public List<Employee> byDepartment(@PathVariable String deptName) {
return empRepo.findByDepartmentName(deptName);
}

@GetMapping("/salary-above/{minSalary}")
public List<Employee> highSalary(@PathVariable Double minSalary) {
return empRepo.findBySalaryGreaterThan(minSalary);
}

@GetMapping("/with-department")
public List<Employee> withDepartment() {
return empRepo.findAllWithDepartment();
}
}

التمرين 5: الاختبار​

اختبار 1: استرجاع جميع الأقسام​

GET http://localhost:8080/api/departments

⚠️ انظر إلى وحدة التحكم SQL:

SELECT * FROM departments;
-- Pas de JOIN! Les employees ne sont pas chargés (LAZY)

اختبار 2: استرجاع قسم مع موظفيه​

GET http://localhost:8080/api/departments/1

وحدة التحكم SQL:

SELECT * FROM departments WHERE id = 1;
SELECT * FROM employees WHERE department_id = 1; -- 2ème requête!

هذا طبيعي مع LAZY!


اختبار 3: تجنب N+1 باستخدام JOIN FETCH​

GET http://localhost:8080/api/departments/with-employees

وحدة التحكم SQL:

-- 1 SEULE requête avec JOIN!
SELECT d.*, e.*
FROM departments d
LEFT JOIN employees e ON d.id = e.department_id;

✅ أكثر كفاءة بكثير!


اختبار 4: موظفو قسم​

GET http://localhost:8080/api/employees/department/IT

النتيجة: Alice, Bob, Charlie


اختبار 5: موظفون براتب > 60000​

GET http://localhost:8080/api/employees/salary-above/60000

النتيجة: Alice (75k), Charlie (80k), Eve (60k)


التحدي: CRUD كامل مع العلاقات​

قم بإنشاء هذه النقاط النهائية:

  1. POST /api/departments - إنشاء قسم
  2. POST /api/employees - إنشاء موظف (مع department_id)
  3. PUT /api/employees/{id}/department/{deptId} - تغيير قسم موظف
  4. DELETE /api/departments/{id} - حذف قسم (وموظفيه بالكاسكيد)
  5. GET /api/departments/{id}/employees - موظفو قسم محدد

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

انتقل إلى TP3 للتقييم الكامل!

➡️ TP6.3: تمرين المطار

⬅️ التمرين السابق: الاتفاقيات

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