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

الفصل 4: الترقيم والأداء

المقدمة​

المشكلة: لديك 100,000 منتج في قاعدة البيانات. هل تعرضهم جميعًا؟

List<Product> products = productRepo.findAll();  // ❌ 100,000 كائن في الذاكرة!

النتيجة:

  • 💥 OutOfMemoryError
  • 🐢 تطبيق بطيء جدًا
  • 😡 مستخدمون غير راضين

هذا الفصل يعلمك كيفية التحسين!

ما ستتعلمه​

  1. الترقيم باستخدام Pageable و Page<T>
  2. الفرز باستخدام Sort
  3. مشكلة N+1 وكيفية حلها
  4. الإسقاطات لتحسين الأداء

المدة: 1 ساعة


الترقيم باستخدام Pageable​

المفهوم​

بدلاً من تحميل كل شيء:

الصفحة 1: [المنتجات 1-20]
الصفحة 2: [المنتجات 21-40]
الصفحة 3: [المنتجات 41-60]
...

يرى المستخدم 20 نتيجة في كل مرة، وليس 100,000!


الكود: المستودع مع الترقيم​

public interface ProductRepository extends JpaRepository<Product, Long> {

// طريقة موروثة
Page<Product> findAll(Pageable pageable);

// طريقتك مع الترقيم
Page<Product> findByCategory(String category, Pageable pageable);

// مع @Query
@Query("SELECT p FROM Product p WHERE p.price < :maxPrice")
Page<Product> findCheap(@Param("maxPrice") Double maxPrice, Pageable pageable);
}

النقاط الرئيسية:

  • معامل Pageable في النهاية
  • العائد Page<T> (ليس List<T>)

الاستخدام: إنشاء Pageable​

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

// الصفحة 0 (الصفحة الأولى)، 20 عنصرًا في كل صفحة
Pageable pageable = PageRequest.of(0, 20);
Page<Product> page = productRepo.findAll(pageable);

// معلومات الصفحة
int totalPages = page.getTotalPages(); // العدد الإجمالي للصفحات
long totalElements = page.getTotalElements(); // العدد الإجمالي للعناصر
List<Product> products = page.getContent(); // الـ 20 منتجًا في الصفحة
boolean hasNext = page.hasNext(); // هل هناك صفحة تالية؟

مثال كامل: API REST مرقمة​

المتحكم:

@RestController
@RequestMapping("/api/products")
public class ProductController {

private final ProductRepository productRepo;

public ProductController(ProductRepository productRepo) {
this.productRepo = productRepo;
}

@GetMapping
public Page<Product> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
Pageable pageable = PageRequest.of(page, size);
return productRepo.findAll(pageable);
}
}

الطلب:

GET /api/products?page=0&size=20

الاستجابة JSON:

{
"content": [
{ "id": 1, "name": "Product 1" },
{ "id": 2, "name": "Product 2" },
...
],
"pageable": {
"pageNumber": 0,
"pageSize": 20
},
"totalPages": 500,
"totalElements": 10000,
"last": false,
"first": true
}

الفرز باستخدام Sort​

الفرز البسيط​

import org.springframework.data.domain.Sort;

// الفرز حسب السعر تصاعديًا
Sort sort = Sort.by("price").ascending();
Page<Product> page = productRepo.findAll(PageRequest.of(0, 20, sort));

// الفرز حسب السعر تنازليًا
Sort sort = Sort.by("price").descending();

الفرز المتعدد​

// الفرز حسب الفئة تصاعديًا، ثم السعر تنازليًا
Sort sort = Sort.by("category").ascending()
.and(Sort.by("price").descending());

Pageable pageable = PageRequest.of(0, 20, sort);
Page<Product> page = productRepo.findAll(pageable);

الفرز في المتحكم​

@GetMapping
public Page<Product> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "ASC") String direction
) {
Sort sort = direction.equalsIgnoreCase("DESC")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();

Pageable pageable = PageRequest.of(page, size, sort);
return productRepo.findAll(pageable);
}

الطلب:

GET /api/products?page=0&size=20&sortBy=price&direction=DESC

مشكلة N+1​

ما هي؟​

المشكلة الأكثر شيوعًا في الأداء في JPA!


مثال على المشكلة​

الكيانات:

@Entity
public class Department {
@Id
private Long id;
private String name;

@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
private List<Employee> employees;
}

@Entity
public class Employee {
@Id
private Long id;
private String name;

@ManyToOne(fetch = FetchType.LAZY)
private Department department;
}

الكود الإشكالي:

List<Department> departments = deptRepo.findAll();  // 1 استعلام

for (Department dept : departments) {
List<Employee> employees = dept.getEmployees(); // N استعلامات!
System.out.println(dept.getName() + ": " + employees.size());
}

SQL المنفذ:

-- 1 استعلام
SELECT * FROM department;

-- N استعلامات (واحد لكل قسم!)
SELECT * FROM employee WHERE department_id = 1;
SELECT * FROM employee WHERE department_id = 2;
SELECT * FROM employee WHERE department_id = 3;
...
SELECT * FROM employee WHERE department_id = N;

إذا كان لديك 100 قسم → 1 + 100 = 101 استعلام! 💥


الحل 1: JOIN FETCH​

@Query("SELECT d FROM Department d JOIN FETCH d.employees")
List<Department> findAllWithEmployees();

SQL المنفذ:

-- 1 استعلام فقط!
SELECT d.*, e.*
FROM department d
LEFT JOIN employee e ON d.id = e.department_id;

✅ 1 استعلام بدلاً من 101!


الحل 2: @EntityGraph​

@EntityGraph(attributePaths = {"employees"})
List<Department> findAll();

يعادل JOIN FETCH ولكنه أبسط!


الحل 3: Batch Size​

إذا لم يكن JOIN FETCH ممكنًا:

@Entity
public class Department {

@OneToMany(mappedBy = "department")
@BatchSize(size = 10) // تحميل 10 أقسام في كل مرة
private List<Employee> employees;
}

SQL المنفذ:

SELECT * FROM department;

-- بدلاً من N استعلامات، فقط N/10!
SELECT * FROM employee WHERE department_id IN (1, 2, 3, ..., 10);
SELECT * FROM employee WHERE department_id IN (11, 12, 13, ..., 20);
...

الإسقاطات لتحسين الأداء​

المشكلة​

أنت تحمل الكيان بالكامل بينما تحتاج فقط إلى حقلين:

List<Product> products = productRepo.findAll();
for (Product p : products) {
System.out.println(p.getName() + ": " + p.getPrice());
// أنت تستخدم فقط name و price!
}

❌ إهدار للذاكرة وعرض النطاق الترددي!


الحل 1: إسقاط الواجهة​

إنشاء واجهة:

public interface ProductSummary {
String getName();
Double getPrice();
}

المستودع:

List<ProductSummary> findAllBy();

الاستخدام:

List<ProductSummary> summaries = productRepo.findAllBy();
for (ProductSummary summary : summaries) {
System.out.println(summary.getName() + ": " + summary.getPrice());
}

✅ تحميل فقط name و price!


الحل 2: DTO مع @Query​

public class ProductDTO {
private String name;
private Double price;

public ProductDTO(String name, Double price) {
this.name = name;
this.price = price;
}
}

المستودع:

@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) FROM Product p")
List<ProductDTO> findAllDTOs();

استراتيجيات الجلب​

FetchType.LAZY (موصى به)​

@ManyToOne(fetch = FetchType.LAZY)
private Department department;

المزايا:

  • ✅ تحميل فقط عند الحاجة
  • ✅ توفير الذاكرة

العيب:

  • ⚠️ قد يسبب N+1

الحل: استخدام JOIN FETCH عند الحاجة


FetchType.EAGER (يجب تجنبه)​

@ManyToOne(fetch = FetchType.EAGER)
private Department department;

المزايا:

  • ✅ البيانات متاحة دائمًا

العيوب:

  • ❌ تحميل حتى لو لم يكن مستخدمًا
  • ❌ قد يحمل الكثير من البيانات

القاعدة: استخدم LAZY افتراضيًا!


الترقيم + الفرز + الأداء: مثال كامل​

public interface ProductRepository extends JpaRepository<Product, Long> {

// الترقيم + الفرز + الإسقاط
@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) " +
"FROM Product p WHERE p.category = :category")
Page<ProductDTO> findByCategoryOptimized(
@Param("category") String category,
Pageable pageable
);

// الترقيم + الفرز + JOIN FETCH
@Query("SELECT p FROM Product p JOIN FETCH p.category")
Page<Product> findAllWithCategory(Pageable pageable);
}

الاستخدام:

// الصفحة 0، 20 عنصرًا، مرتبة حسب السعر تنازليًا
Pageable pageable = PageRequest.of(0, 20, Sort.by("price").descending());

// تحميل محسن
Page<ProductDTO> page = productRepo.findByCategoryOptimized("Electronics", pageable);

System.out.println("الإجمالي: " + page.getTotalElements());
System.out.println("الصفحات: " + page.getTotalPages());

for (ProductDTO dto : page.getContent()) {
System.out.println(dto.getName() + ": " + dto.getPrice());
}

نصائح الأداء​

1. دائمًا قم بترقيم القوائم الكبيرة​

// ❌ سيء
List<Product> products = productRepo.findAll(); // كل المنتجات!

// ✅ جيد
Page<Product> page = productRepo.findAll(PageRequest.of(0, 20));

2. استخدام الإسقاطات​

// ❌ تحميل الكيان بالكامل
List<Product> products = productRepo.findAll();

// ✅ تحميل فقط الحقول اللازمة
List<ProductSummary> summaries = productRepo.findAllBy();

3. تجنب N+1​

// ❌ مشكلة N+1
List<Employee> employees = empRepo.findAll();
employees.forEach(e -> System.out.println(e.getDepartment().getName()));

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

4. استخدام LAZY افتراضيًا​

// ✅ جيد
@ManyToOne(fetch = FetchType.LAZY)
private Department department;

// ❌ تجنب
@ManyToOne(fetch = FetchType.EAGER)
private Department department;

5. تحديد عمق الربط​

// ❌ الكثير من الربط
@Query("SELECT e FROM Employee e " +
"JOIN FETCH e.department d " +
"JOIN FETCH d.company c " +
"JOIN FETCH c.industry i")

// ✅ فقط ما هو ضروري
@Query("SELECT e FROM Employee e JOIN FETCH e.department")

مراقبة الاستعلامات​

تفعيل سجلات SQL​

application.properties:

# عرض الاستعلامات SQL
spring.jpa.show-sql=true

# تنسيق SQL (مقروء)
spring.jpa.properties.hibernate.format_sql=true

# عرض المعاملات
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

# إحصائيات Hibernate
spring.jpa.properties.hibernate.generate_statistics=true

الكونسول:

Hibernate: 
select
product0_.id as id1_0_,
product0_.name as name2_0_,
product0_.price as price3_0_
from
product product0_ limit ?

ملخص الفصل​

الترقيم​

Pageable pageable = PageRequest.of(page, size, sort);
Page<Entity> page = repo.findAll(pageable);

الفرز​

Sort sort = Sort.by("field").ascending();
Sort multi = Sort.by("field1").and(Sort.by("field2").descending());

مشكلة N+1​

// ❌ مشكلة
List<Entity> list = repo.findAll();
list.forEach(e -> e.getRelation());

// ✅ الحل
@Query("SELECT e FROM Entity e JOIN FETCH e.relation")
List<Entity> findAllWithRelation();

الإسقاطات​

// واجهة
public interface Summary {
String getName();
}

// DTO
@Query("SELECT new DTO(e.name) FROM Entity e")
List<DTO> findDTOs();

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

  1. كيف تنشئ صفحة تحتوي على 20 عنصرًا مرتبة حسب السعر؟
  2. ما هي مشكلة N+1؟
  3. كيف تحلها؟
  4. لماذا تستخدم الإسقاطات؟

الإجابات:

  1. PageRequest.of(0, 20, Sort.by("price").ascending())
  2. استعلام رئيسي واحد + N استعلامات للعلاقات
  3. JOIN FETCH أو @EntityGraph
  4. لتحميل فقط الحقول اللازمة

تهانينا! 🎉​

لقد أكملت كل النظرية في الوحدة 6!

أنت الآن تتقن:

  • ✅ 15+ كلمة مفتاحية من الاتفاقيات
  • ✅ الأنواع الثلاثة للعلاقات
  • ✅ @Query (JPQL و SQL)
  • ✅ الترقيم والأداء

الخطوة التالية: التطبيق العملي مع التدريبات!


➡️ ابدأ TP6.1: الاتفاقيات المتقدمة

⬅️ الفصل السابق: @Query

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