JPQL وواجهة Criteria والاستعلامات

المعاملات والاستعلامات المسماة

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

المعاملات والاستعلامات المسماة

كل استعلام JPQL عملي يحتاج إلى قيم خارجية — معرّف مستخدم للتصفية، سلسلة حالة للمطابقة، نطاق تاريخ للبحث فيه. دمج هذه القيم مباشرةً كنصوص حرفية في الاستعلام يُعدّ غير آمن (حقن SQL عبر JPQL) وغير فعّال (لأن مزوّد الثبات لن يتمكّن من إعادة استخدام خطة الاستعلام المُصرَّفة). لذلك يوفّر JPQL أسلوبَين لربط المعاملات، ويستكملهما JPA بآلية @NamedQuery التي تُعلن الاستعلامات مسبقًا عند تحميل الفئة.

المعاملات الموضعية

المعاملات الموضعية هي عناصر نائبة تُكتب كـ ?1 و?2 و... (تبدأ من الرقم واحد). تُربط القيم عبر TypedQuery.setParameter(int, Object).

import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; import java.util.List; public class OrderRepository { private final EntityManager em; public OrderRepository(EntityManager em) { this.em = em; } public List<Order> findByCustomerAndStatus(Long customerId, String status) { TypedQuery<Order> q = em.createQuery( "SELECT o FROM Order o WHERE o.customer.id = ?1 AND o.status = ?2", Order.class ); q.setParameter(1, customerId); q.setParameter(2, status); return q.getResultList(); } }

المعاملات الموضعية موجزة لكنها هشّة — إذا أعدت ترتيب جملة WHERE يجب أيضًا إعادة ترقيم كل استدعاء setParameter. لأي شيء أكثر تعقيدًا من سطر واحد، تكون المعاملات المسماة هي الخيار الأفضل دائمًا تقريبًا.

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

تستخدم المعاملات المسماة بادئة النقطتين :name وتُربط عبر setParameter(String, Object). الاسم يوثّق نفسه بنفسه ومستقل عن الموضع.

public List<Order> findByCustomerAndStatus(Long customerId, String status) { return em.createQuery( "SELECT o FROM Order o " + "WHERE o.customer.id = :customerId AND o.status = :status", Order.class) .setParameter("customerId", customerId) .setParameter("status", status) .getResultList(); }
استخدم المعاملات المسماة دائمًا في كود الإنتاج. فهي توضّح النية في موقع الاستدعاء، وتصمد أمام إعادة ترتيب الجمل دون أن تنكسر، وهي إلزامية عند استخدام @NamedQuery.

الأنواع الزمنية والأنواع الخاصة

عند ربط java.util.Date (الواجهة البرمجية القديمة) كان يجب توفير تلميح TemporalType. مع واجهة java.time الحديثة — LocalDate وLocalDateTime وInstant — يعالجها Hibernate 6 بشكل أصلي، فلا حاجة لأي تلميح.

import java.time.LocalDate; public List<Order> findOrdersAfter(LocalDate since) { return em.createQuery( "SELECT o FROM Order o WHERE o.placedAt >= :since", Order.class) .setParameter("since", since) // لا حاجة لـ TemporalType مع java.time .getResultList(); }
تمرير null كمعامل آمن. تمرير null إلى setParameter يُصدر IS NULL في SQL المُولَّد — لا تحتاج إلى فرع استعلام منفصل للمرشّحات الاختيارية عند استخدام Criteria API، لكن مع استعلامات JPQL النصية تكتب عادةً فحوصات null صريحة في الـ JPQL أو تستخدم أساليب استعلام منفصلة.

التصفيح: setFirstResult و setMaxResults

يُتحكَّم في التصفيح عبر TypedQuery بأسلوب سلسلي، وليس من خلال جمل خاصة بلهجة SQL مثل LIMIT/OFFSET. هذا يُبقي JPQL محمولًا عبر قواعد البيانات المختلفة.

public List<Order> findPage(String status, int page, int pageSize) { return em.createQuery( "SELECT o FROM Order o WHERE o.status = :status ORDER BY o.placedAt DESC", Order.class) .setParameter("status", status) .setFirstResult(page * pageSize) // إزاحة تبدأ من الصفر .setMaxResults(pageSize) .getResultList(); }

إعلان الاستعلامات المسماة بـ @NamedQuery

الاستعلام المسمّى هو سلسلة JPQL تُعلَن مرة واحدة — على فئة الكيان — ويُشار إليها باسم منطقي في كل مكان آخر. يُحلّل JPA ويتحقق من صحة JPQL عند بدء تشغيل التطبيق (عند بناء EntityManagerFactory)، وليس في وقت التشغيل. هذا يعني اكتشاف أخطاء الصياغة عند الإقلاع، وليس عند أول طلب مستخدم.

import jakarta.persistence.*; @Entity @Table(name = "orders") @NamedQuery( name = "Order.findByStatus", query = "SELECT o FROM Order o WHERE o.status = :status ORDER BY o.placedAt DESC" ) @NamedQuery( name = "Order.findByCustomer", query = "SELECT o FROM Order o WHERE o.customer.id = :customerId" ) public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String status; @Column(name = "placed_at") private java.time.LocalDateTime placedAt; @ManyToOne(fetch = FetchType.LAZY) private Customer customer; // getters / setters محذوفة }

تستخدم الاستعلامات المسماة المتعددة على نفس الكيان @NamedQueries({ @NamedQuery(...), @NamedQuery(...) })، أو في Java 8 وما بعده يمكنك ببساطة تكرار التعليق التوضيحي كما هو موضح أعلاه (دعم التعليقات التوضيحية القابلة للتكرار).

تنفيذ الاستعلام المسمّى

استدع em.createNamedQuery(name, resultClass) بدلًا من em.createQuery(jpqlString, resultClass). ربط المعاملات متطابق تمامًا.

public List<Order> findByStatus(String status) { return em.createNamedQuery("Order.findByStatus", Order.class) .setParameter("status", status) .getResultList(); }
الاتفاقية: سمّ استعلاماتك المسماة EntityName.methodName (مثل Order.findByStatus). يبحث Spring Data JPA عن استعلام مسمّى باتباع هذه الاتفاقية تلقائيًا قبل توليد استعلام من اسم الأسلوب، لذا الاسم مفيد مضاعف.

الاستعلامات المسماة في مستودعات Spring Data JPA

إذا كنت تستخدم Spring Data JPA، فإن أسلوب مستودع باسم findByStatus على مستودع Order يحلّ الاستعلام المسمّى Order.findByStatus تلقائيًا إذا وُجد. يمكنك أيضًا تزيين أسلوب المستودع بـ @Query كبديل مضمَّن، لكن الاستعلامات المسماة المُعلَنة مسبقًا تتميّز بالتحقق من الصحة وقت الإقلاع.

import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface OrderRepository extends JpaRepository<Order, Long> { // Spring Data يجد @NamedQuery("Order.findByStatus") تلقائيًا List<Order> findByStatus(String status); // أو مضمَّنًا بـ @Query — يُتحقق منه وقت التشغيل لا وقت الإقلاع // @Query("SELECT o FROM Order o WHERE o.customer.id = :id") // List<Order> findByCustomerId(@Param("id") Long id); }

اعتبارات الأداء

  • تخزين خطة الاستعلام في الذاكرة المؤقتة: عند استخدام ربط المعاملات (بكلا الأسلوبين) يُصرف مزوّد الثبات JPQL مرة واحدة ويخزّن الخطة. تسلسل القيم مباشرةً في سلسلة JPQL يُجبر على إعادة التصريف عند كل استدعاء ويُفقد هذه الذاكرة المؤقتة كليًا.
  • الاستعلامات المسماة تُصرَّف مرة واحدة عند الإقلاع: خطتها تُعاد استخدامها عند كل استدعاء دون أي حمل تحليل، مما يجعلها مسار تنفيذ JPQL الأكثر كفاءة.
  • استخدم getSingleResult بحذر: يرمي NoResultException إذا لم تتطابق أي سجلات، وNonUniqueResultException عند وجود سجلات متعددة. لفّه أو استخدم getResultList() وافحص isEmpty() إذا كان عدم وجود نتائج ممكنًا.
لا تبنِ مطلقًا سلاسل JPQL بتسلسل النصوص مع مدخلات المستخدم. حقن JPQL هجوم حقيقي — يمكن للمهاجم الهروب من المسند المقصود وإلحاق JPQL عشوائي. استخدم دائمًا المعاملات الموضعية أو المسماة لتمرير القيم الخارجية بأمان.

الخلاصة

استخدم المعاملات المسماة (:name) بدلًا من الموضعية للقراءة والمتانة. استفد من setFirstResult/setMaxResults للتصفيح المحمول عبر قواعد البيانات. أعلن الاستعلامات المُنفَّذة كثيرًا أو المشتركة كـ @NamedQuery على فئة الكيان للحصول على التحقق من الصياغة وقت الإقلاع وأفضل إعادة استخدام لخطة الاستعلام. في الدرس القادم ستجمع هذه التقنيات مع دوال التجميع والتجميع والجمل HAVING.