Spring Data JPA

توابع الاستعلام المشتقّة

18 دقيقة الدرس 4 من 13

توابع الاستعلام المشتقّة

من أكثر ميزات Spring Data JPA إثارةً للإعجاب قدرتُه على توليد استعلام JPQL كامل — والـ SQL الناتج عنه — من مجرّد اسم تابع Java. أنت تُعلن عن النية؛ و Spring Data يشتقّ التنفيذ عند بدء تشغيل التطبيق. لا حاجة لـ @Query، ولا SQL، ولا تمديد EntityManager.

يشرح هذا الدرس بالتفصيل كيف يعمل هذا الاشتقاق، وما الكلمات المفتاحية المتاحة، وكيف يبدو الـ SQL المُولَّد، وأين يبدأ هذا النهج بالإيذاء — حتى تستخدمه بثقة دون مفاجآت في بيئة الإنتاج.

كيف تقرأ Spring Data اسم التابع

عند بدء تشغيل سياق التطبيق، تفحص Spring Data كل تابع مُعلَن في واجهة المستودع الذي لا يحمل تعليقًا توضيحيًا @Query ولا تنفيذًا مكتوبًا يدويًا. تحلّل الاسم وفق قواعد نحوية ثابتة:

  1. الكلمة المفتاحية للموضوع — ما الفعل المطلوب: find…By، count…By، exists…By، delete…By، sum…By.
  2. المعايير — الشرط الذي يأتي بعد By: تعبير خاصية واحد أو أكثر مربوطة بـ And / Or.
  3. المعاملات — تُلحق باسم الخاصية: Like، IgnoreCase، Between، LessThan، IsNull، In، وكثير غيرها.
  4. نوع الإرجاع — يُستنتج من نوع الإرجاع Java المُعلَن: كيان واحد، Optional<T>، List<T>، Page<T>، نوع بدائي لـ count، إلخ.

إذا تعذّر على المحلّل اللغوي تحليل مسار الخاصية مقابل الكيان المُدار يرمي PropertyReferenceException عند بدء التشغيل — وهو فشل سريع يمنع الأخطاء الصامتة في وقت التشغيل.

الكيان الذي سنعمل عليه

جميع الأمثلة أدناه تستخدم هذا الكيان:

package com.example.shop.domain; import jakarta.persistence.*; import java.math.BigDecimal; import java.time.LocalDate; @Entity @Table(name = "orders") public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String customerName; private String status; // "PENDING", "SHIPPED", "CANCELLED" private BigDecimal totalAmount; private LocalDate orderDate; private boolean active; // getters/setters محذوفة للإيجاز }

findBy — النمط الأساسي

كل تابع findBy يُقابل عبارة SELECT … FROM orders WHERE …. الصيغة الأبسط تُصفّي على خاصية واحدة:

public interface OrderRepository extends JpaRepository<Order, Long> { // SELECT o FROM Order o WHERE o.customerName = ?1 List<Order> findByCustomerName(String name); // SELECT o FROM Order o WHERE o.status = ?1 Optional<Order> findFirstByStatus(String status); // SELECT o FROM Order o WHERE o.status = ?1 AND o.active = ?2 List<Order> findByStatusAndActive(String status, boolean active); // SELECT o FROM Order o WHERE o.totalAmount > ?1 List<Order> findByTotalAmountGreaterThan(BigDecimal amount); // SELECT o FROM Order o WHERE o.orderDate BETWEEN ?1 AND ?2 List<Order> findByOrderDateBetween(LocalDate from, LocalDate to); // يجب أن يُوفّر المستدعي % بنفسه: مثال "%smith%" List<Order> findByCustomerNameContainingIgnoreCase(String fragment); }
تحلّ Spring Data مسارات الخصائص لا أسماء الأعمدة. الكلمة المفتاحية CustomerName في اسم التابع تُقابل حقل Java هو customerName، الذي يُحوّله Hibernate إلى العمود customer_name (بصيغة snake_case افتراضيًا). لا تكتب أسماء أعمدة SQL قطّ في توابع الاشتقاق.

countBy — العدّ دون جلب البيانات

يُصدر countBy عبارة SELECT COUNT(…) بدلًا من تحديد حالة الكيان. يجب أن يكون نوع الإرجاع long أو Long:

// SELECT COUNT(o) FROM Order o WHERE o.status = ?1 long countByStatus(String status); // SELECT COUNT(o) FROM Order o WHERE o.active = true long countByActiveTrue(); // SELECT COUNT(o) FROM Order o WHERE o.customerName = ?1 AND o.status = ?2 long countByCustomerNameAndStatus(String name, String status);

لأنه لا تُحمَّل حالة الكيان، يُعدّ countBy دومًا أرخص من جلب List واستدعاء .size(). هذا تمييز أداء مهم: يصل SELECT COUNT من قاعدة البيانات كرقم واحد؛ أما بديل SELECT * فيُسلسل كل عمود لكل صف متطابق عبر الشبكة إلى ذاكرة JVM الكومية (heap).

existsBy — فحص منطقي بوليانيّ

حين تحتاج فقط معرفة ما إذا كان صفٌّ موجودًا، يكون existsBy أرخص حتى من countBy. يُترجمه Hibernate إلى SELECT COUNT(*) > 0 (أو صيغة CASE WHEN EXISTS … بحسب اللهجة)، وتُحوّل Spring Data النتيجة إلى boolean:

// SELECT CASE WHEN COUNT(o) > 0 THEN TRUE ELSE FALSE END // FROM Order o WHERE o.customerName = ?1 boolean existsByCustomerName(String name);

deleteBy — الحذف المجمّع

يجب أن تعمل توابع deleteBy داخل معاملة (transaction). تجلب أولًا الكيانات المطابقة ثم تستدعي remove() على كل منها — مما يعني أن Hibernate يُطلق دوالّ ردّ النداء (@PreRemove) ويُكرّم سلاسل الاستدعاء (cascades). هذا مقصود لكنه قد يكون مكلفًا إذا تطابقت صفوف كثيرة.

@Transactional long deleteByStatus(String status);
يجلب deleteBy الكيانات قبل حذفها. لمجموعات البيانات الكبيرة يُطلق هذا جلبًا بشرط IN يعقبه حذف فردي لكل سجل. إن احتجت عبارة DELETE FROM orders WHERE status = ? المجمّعة دون تحميل الكيانات، استخدم @Query مع @Modifying بدلًا من ذلك — فهو يُنفّذ عبارة SQL واحدة مباشرةً.

مرجع الكلمات المفتاحية للمعاملات

تتحد الكلمات المفتاحية التي تأتي بعد اسم الخاصية لتُكوّن شروطًا دقيقة:

  • Is / Equals= ? (افتراضي حين لا توجد كلمة مفتاحية)
  • Not<> ?
  • LessThan / LessThanEqual< ? / <= ?
  • GreaterThan / GreaterThanEqual> ? / >= ?
  • BetweenBETWEEN ? AND ? (معاملان)
  • Like / NotLikeLIKE ? (يجب أن تُدرج % في القيمة)
  • Containing — يُلفّ المُعطى تلقائيًا في %…%
  • StartingWith / EndingWith?% / %?
  • In / NotInIN (?) (المعامل Collection)
  • IsNull / IsNotNullIS NULL / IS NOT NULL (بلا معامل)
  • True / False= TRUE / = FALSE (بلا معامل)
  • IgnoreCase — يُغلّف كلا الجانبين في LOWER()

تحديد عدد النتائج

تدعم Spring Data استخدام Top وFirst كمحدّدات لحجم النتيجة مباشرةً في اسم التابع:

// SELECT … ORDER BY order_date DESC LIMIT 1 Optional<Order> findFirstByStatusOrderByOrderDateDesc(String status); // SELECT … ORDER BY total_amount DESC LIMIT 5 List<Order> findTop5ByActiveOrderByTotalAmountDesc(boolean active);

متى تكون الاستعلامات المشتقّة الأداة الصحيحة — ومتى لا تكون

تتألّق الاستعلامات المشتقّة في مرشّحات الكيان الواحدة البسيطة. لكنها تُصبح عبئًا حين:

  • يتجاوز اسم التابع ~4 شروط — عندها يصبح أصعب قراءةً من JPQL المكافئ.
  • تحتاج JOIN عبر كيانات متعددة — يدعم المحلّل التنقّل بالنقطة (findByCustomer_Email) لكن المسارات العميقة هشّة.
  • تحتاج شروطًا ديناميكية تعتمد على حالة وقت التشغيل — استخدم Specification API أو @Query بمعاملات مُسمّاة.
  • يتطلّب الأداء تلميح فهرسة محدّدًا، أو FETCH JOIN لتجنّب مشكلة N+1، أو استعلامًا أصليًا — مرّة أخرى، الجأ إلى @Query.
فعّل تسجيل SQL أثناء التطوير بإضافة spring.jpa.show-sql=true وspring.jpa.properties.hibernate.format_sql=true إلى application.properties. فحص الـ SQL المُولَّد هو أسرع طريقة لاكتشاف ضرب ديكارتي عرضي أو ضربة فهرس مفقودة.

الخلاصة

تُحلّل Spring Data JPA أسماء التوابع عند بدء التشغيل وتشتقّ الـ JPQL المقابل — والـ SQL في نهاية المطاف — تلقائيًا. يُولّد findBy عبارات SELECT، ويُولّد countBy استعلامات COUNT كفؤة، ويُعدّ existsBy أرخص فحص للوجود، بينما تغطّي مجموعة غنية من الكلمات المفتاحية معظم الشروط الشائعة. استخدم الاستعلامات المشتقّة لمرشّحات الكيان الواحد الموجزة والمقروءة، وانتقل إلى @Query أو Specification API فور أن يصبح اسم التابع مُعقّدًا أو يتطلّب ضبط الأداء JPQL صريحًا.