الفصل 3: استعلامات مخصصة باستخدام @Query
مقدمة
تعتبر قواعد التسمية قوية، لكنها أحيانًا غير كافية للاستعلامات المعقدة.
هذا الفصل يعلمك كيفية كتابة استعلاماتك الخاصة باستخدام @Query!
ما ستتعلمه
- صياغة JPQL (لغة استعلامات جافا بيرسيستنس)
- @Query مع JPQL
- @Query مع SQL الأصلي
- المعلمات المسماة مع @Param
- استعلامات التعديل (@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
الفروقات الأساسية
| الجانب | JPQL | SQL |
|---|---|---|
| الهدف | كيانات جافا | جداول قاعدة البيانات |
| الصياغة | FROM Product p | FROM product |
| الحقول | p.name | product.name |
| العلاقات | p.category.name | JOIN ضروري |
| قابلية النقل | ✅ نعم | ❌ يعتمد على قاعدة البيانات |
مثال مقارن
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);
التقييم الذاتي
- ما الفرق بين JPQL و SQL؟
- كيف تمرر المعلمات المسماة؟
- ماذا يفعل
JOIN FETCH؟ - متى تستخدم
@Modifying؟
الإجابات:
- JPQL يستخدم كيانات جافا، SQL يستخدم جداول قاعدة البيانات
- باستخدام
:nomParamو@Param("nomParam") - يحمل العلاقات في استعلام واحد (يتجنب N+1)
- للتحديث أو الحذف
الخطوة التالية
أنت الآن تتقن @Query! انتقل إلى التقسيم والأداء.