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

الفصل 3: استعلامات مخصصة باستخدام @Query

مقدمة​

تعتبر قواعد التسمية قوية، لكنها أحيانًا غير كافية للاستعلامات المعقدة.

هذا الفصل يعلمك كيفية كتابة استعلاماتك الخاصة باستخدام @Query!

ما ستتعلمه​

  1. صياغة JPQL (لغة استعلامات جافا بيرسيستنس)
  2. @Query مع JPQL
  3. @Query مع SQL الأصلي
  4. المعلمات المسماة مع @Param
  5. استعلامات التعديل (@Modifying)

المدة: 1 ساعة


لماذا @Query؟​

حدود القواعد​

حاول إنشاء هذا الاستعلام باستخدام القواعد:

SELECT p FROM Product p 
WHERE p.price < :maxPrice
AND p.stock > 0
AND p.category.name IN ('Electronics', 'Computers')
ORDER BY p.price ASC

اسم الطريقة الناتج:

findByPriceLessThanAndStockGreaterThanAndCategoriesNameInOrderByPriceAsc(
Double maxPrice,
Integer minStock,
List<String> categories
)

❌ طويل جدًا! غير قابل للقراءة!


الحل: @Query​

@Query("SELECT p FROM Product p " +
"WHERE p.price < :maxPrice " +
"AND p.stock > 0 " +
"AND p.category.name IN :categories " +
"ORDER BY p.price ASC")
List<Product> findAvailableProducts(
@Param("maxPrice") Double maxPrice,
@Param("categories") List<String> categories
);

✅ واضح! قابل للقراءة! قوي!


JPQL مقابل SQL​

الفروقات الأساسية​

الجانبJPQLSQL
الهدفكيانات جافاجداول قاعدة البيانات
الصياغةFROM Product pFROM product
الحقولp.nameproduct.name
العلاقاتp.category.nameJOIN ضروري
قابلية النقل✅ نعم❌ يعتمد على قاعدة البيانات

مثال مقارن​

JPQL (موصى به):

@Query("SELECT p FROM Product p WHERE p.price < :max")
List<Product> findCheap(@Param("max") Double max);

SQL الأصلي:

@Query(value = "SELECT * FROM product WHERE price < :max", 
nativeQuery = true)
List<Product> findCheap(@Param("max") Double max);

يفضل JPQL: أكثر قابلية للنقل، أكثر توجهًا للكائنات!


JPQL: الأساسيات​

1. SELECT بسيط​

@Query("SELECT p FROM Product p")
List<Product> findAllProducts();

// مكافئ SQL:
// SELECT * FROM product

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

  • Product = اسم الفئة (ليس الجدول)
  • p = اسم مستعار للكيان

2. شروط WHERE​

@Query("SELECT p FROM Product p WHERE p.price < :maxPrice")
List<Product> findByPriceLessThan(@Param("maxPrice") Double maxPrice);

@Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword%")
List<Product> searchByName(@Param("keyword") String keyword);

@Query("SELECT p FROM Product p WHERE p.active = true AND p.stock > 0")
List<Product> findAvailable();

3. ORDER BY​

@Query("SELECT p FROM Product p ORDER BY p.price ASC")
List<Product> findAllOrderByPrice();

@Query("SELECT p FROM Product p ORDER BY p.price DESC, p.name ASC")
List<Product> findAllSorted();

4. الربط​

بدون JOIN صريح (يستخدم العلاقة):

@Query("SELECT e FROM Employee e WHERE e.department.name = :deptName")
List<Employee> findByDepartmentName(@Param("deptName") String deptName);

مع JOIN:

@Query("SELECT e FROM Employee e JOIN e.department d WHERE d.name = :deptName")
List<Employee> findByDepartmentName(@Param("deptName") String deptName);

مع JOIN FETCH (يتجنب N+1):

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

المعلمات: الموضعية مقابل المسماة​

المعلمات الموضعية (?1, ?2)​

@Query("SELECT p FROM Product p WHERE p.price BETWEEN ?1 AND ?2")
List<Product> findByPriceRange(Double min, Double max);

المشكلة: الترتيب مهم، غير واضح


المعلمات المسماة (:name) - موصى بها​

@Query("SELECT p FROM Product p WHERE p.price BETWEEN :min AND :max")
List<Product> findByPriceRange(
@Param("min") Double minPrice,
@Param("max") Double maxPrice
);

✅ المزايا:

  • أكثر وضوحًا
  • مستقل عن الترتيب
  • أقل أخطاء

استعلامات معقدة​

مثال 1: بحث متعدد المعايير​

@Query("SELECT p FROM Product p " +
"WHERE (:name IS NULL OR p.name LIKE %:name%) " +
"AND (:minPrice IS NULL OR p.price >= :minPrice) " +
"AND (:maxPrice IS NULL OR p.price <= :maxPrice) " +
"AND (:category IS NULL OR p.category = :category)")
List<Product> searchProducts(
@Param("name") String name,
@Param("minPrice") Double minPrice,
@Param("maxPrice") Double maxPrice,
@Param("category") String category
);

الاستخدام:

// جميع المعلمات
productRepo.searchProducts("Laptop", 500.0, 1500.0, "Electronics");

// فقط الاسم
productRepo.searchProducts("Laptop", null, null, null);

// السعر والفئة
productRepo.searchProducts(null, 500.0, 1500.0, "Electronics");

مثال 2: التجميعات​

// العد
@Query("SELECT COUNT(p) FROM Product p WHERE p.category = :category")
Long countByCategory(@Param("category") String category);

// المجموع
@Query("SELECT SUM(p.price) FROM Product p WHERE p.category = :category")
Double sumPricesByCategory(@Param("category") String category);

// المتوسط
@Query("SELECT AVG(p.price) FROM Product p")
Double getAveragePrice();

// الأدنى/الأعلى
@Query("SELECT MIN(p.price), MAX(p.price) FROM Product p")
Object[] getPriceRange();

مثال 3: GROUP BY و HAVING​

@Query("SELECT p.category, COUNT(p) FROM Product p " +
"GROUP BY p.category " +
"HAVING COUNT(p) > :minCount")
List<Object[]> getCategoriesWithMinProducts(@Param("minCount") Long minCount);

النتيجة:

List<Object[]> results = productRepo.getCategoriesWithMinProducts(5L);
for (Object[] row : results) {
String category = (String) row[0];
Long count = (Long) row[1];
System.out.println(category + ": " + count + " منتجات");
}

الإسقاطات: إرجاع DTOs​

المشكلة​

لا تريد الكيان بالكامل، فقط بعض الحقول:

// ❌ يحمل الكيان بالكامل
@Query("SELECT p FROM Product p")
List<Product> findAll();

الحل 1: Object[]​

@Query("SELECT p.name, p.price FROM Product p")
List<Object[]> findNameAndPrice();

الاستخدام:

List<Object[]> results = productRepo.findNameAndPrice();
for (Object[] row : results) {
String name = (String) row[0];
Double price = (Double) row[1];
}

❌ غير آمن من ناحية النوع!


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

إنشاء واجهة:

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

الاستخدام في @Query:

@Query("SELECT p.name AS name, p.price AS price FROM Product p")
List<ProductSummary> findSummaries();

الاستخدام:

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

✅ آمن من ناحية النوع!


الحل 3: DTO مع منشئ​

إنشاء DTO:

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

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

// Getters
}

الاستخدام في @Query:

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

✅ الأفضل! آمن من ناحية النوع وفعال!


SQL الأصلي مع nativeQuery = true​

متى تستخدم SQL الأصلي؟​

استخدم SQL الأصلي لـ:

  • ✅ وظائف خاصة بقاعدة البيانات (PostgreSQL، MySQL، إلخ.)
  • ✅ استعلامات معقدة جدًا
  • ✅ تحسينات خاصة

تجنب لـ:

  • ❌ استعلامات بسيطة (استخدم JPQL)
  • ❌ قابلية النقل بين قواعد البيانات

صياغة SQL الأصلي​

@Query(value = "SELECT * FROM product WHERE price < :maxPrice", 
nativeQuery = true)
List<Product> findCheapNative(@Param("maxPrice") Double maxPrice);

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

  • nativeQuery = true
  • أسماء الجداول والأعمدة في قاعدة البيانات (ليست كيانات جافا)

مثال: وظيفة PostgreSQL​

@Query(value = "SELECT * FROM product " +
"WHERE LOWER(name) = LOWER(:name)",
nativeQuery = true)
List<Product> findByNameCaseInsensitive(@Param("name") String name);

استعلامات التعديل: @Modifying​

تحديث​

@Modifying
@Transactional
@Query("UPDATE Product p SET p.price = :newPrice WHERE p.id = :id")
int updatePrice(@Param("id") Long id, @Param("newPrice") Double newPrice);

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

  • @Modifying إلزامي
  • @Transactional إلزامي
  • يعيد عدد الصفوف المعدلة

حذف​

@Modifying
@Transactional
@Query("DELETE FROM Product p WHERE p.active = false")
int deleteInactiveProducts();

انتباه: عدم تناسق الكاش​

المشكلة:

Product p = productRepo.findById(1L);  // Price = 100
productRepo.updatePrice(1L, 200.0); // تحديث في قاعدة البيانات
// لكن p.getPrice() لا يزال يعيد 100! (الكاش)

الحل: تفريغ الكاش

@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Product p SET p.price = :price WHERE p.id = :id")
int updatePrice(@Param("id") Long id, @Param("price") Double price);

الاستعلامات المسماة​

تعريف في الكيان​

@Entity
@NamedQuery(
name = "Product.findExpensive",
query = "SELECT p FROM Product p WHERE p.price > :minPrice"
)
public class Product {
// ...
}

الاستخدام في المستودع​

public interface ProductRepository extends JpaRepository<Product, Long> {
// يجد Spring الاستعلام المسماة تلقائيًا!
List<Product> findExpensive(@Param("minPrice") Double minPrice);
}

أمثلة كاملة​

مستودع المنتجات الكامل​

public interface ProductRepository extends JpaRepository<Product, Long> {

// 1. بحث بسيط
@Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword%")
List<Product> searchByName(@Param("keyword") String keyword);

// 2. بحث مع الربط
@Query("SELECT p FROM Product p JOIN FETCH p.category WHERE p.price < :max")
List<Product> findCheapWithCategory(@Param("max") Double max);

// 3. تجميع
@Query("SELECT p.category.name, COUNT(p), AVG(p.price) " +
"FROM Product p GROUP BY p.category.name")
List<Object[]> getStatsByCategory();

// 4. إسقاط DTO
@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) " +
"FROM Product p WHERE p.active = true")
List<ProductDTO> findActiveDTOs();

// 5. SQL الأصلي
@Query(value = "SELECT * FROM product WHERE stock < 10",
nativeQuery = true)
List<Product> findLowStock();

// 6. تعديل
@Modifying
@Transactional
@Query("UPDATE Product p SET p.active = false WHERE p.stock = 0")
int deactivateOutOfStock();
}

أفضل الممارسات​

1. أسماء طرق واضحة​

// ✅ جيد
@Query("SELECT p FROM Product p WHERE p.price < :max")
List<Product> findByPriceLessThan(@Param("max") Double max);

// ❌ سيء
@Query("SELECT p FROM Product p WHERE p.price < :max")
List<Product> get(@Param("max") Double max);

2. المعلمات المسماة​

// ✅ جيد
@Query("... WHERE p.price BETWEEN :min AND :max")
List<Product> find(@Param("min") Double min, @Param("max") Double max);

// ❌ سيء
@Query("... WHERE p.price BETWEEN ?1 AND ?2")
List<Product> find(Double min, Double max);

3. JOIN FETCH للأداء​

// ❌ مشكلة N+1
@Query("SELECT e FROM Employee e")
List<Employee> findAll();
// ثم e.getDepartment() يقوم باستعلام لكل موظف!

// ✅ استعلام واحد فقط
@Query("SELECT e FROM Employee e JOIN FETCH e.department")
List<Employee> findAllWithDepartment();

4. JPQL مقابل SQL الأصلي​

// ✅ يفضل JPQL (قابل للنقل)
@Query("SELECT p FROM Product p WHERE p.price < :max")

// ⚠️ SQL الأصلي فقط إذا لزم الأمر
@Query(value = "SELECT * FROM product WHERE price < :max", nativeQuery = true)

ملخص الفصل​

@Query يسمح​

✅ استعلامات معقدة أكثر قابلية للقراءة
✅ JPQL (موجه للكائنات، قابل للنقل)
✅ SQL الأصلي (وظائف خاصة بقاعدة البيانات)
✅ إسقاطات (DTOs، واجهات)
✅ تجميعات (COUNT، SUM، AVG)
✅ تعديلات (@Modifying)


صياغة JPQL​

@Query("SELECT e FROM Entity e " +
"WHERE e.field = :param " +
"ORDER BY e.field ASC")
List<Entity> findMethod(@Param("param") Type param);

SQL الأصلي​

@Query(value = "SELECT * FROM table WHERE column = :param", 
nativeQuery = true)
List<Entity> findMethod(@Param("param") Type param);

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

  1. ما الفرق بين JPQL و SQL؟
  2. كيف تمرر المعلمات المسماة؟
  3. ماذا يفعل JOIN FETCH؟
  4. متى تستخدم @Modifying؟

الإجابات:

  1. JPQL يستخدم كيانات جافا، SQL يستخدم جداول قاعدة البيانات
  2. باستخدام :nomParam و @Param("nomParam")
  3. يحمل العلاقات في استعلام واحد (يتجنب N+1)
  4. للتحديث أو الحذف

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

أنت الآن تتقن @Query! انتقل إلى التقسيم والأداء.

➡️ الفصل 4: التقسيم والأداء

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

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