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

الاستعلام باستخدام JPQL

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

الاستعلام باستخدام JPQL

كل تطبيق يحفظ البيانات يحتاج في نهاية المطاف إلى استرجاعها بطرق مرنة ومصفّاة ومرتّبة. يمكنك اللجوء إلى SQL الأصلية — لكن هذا يعني ربط الكود بأسماء جداول وأعمدة محددة، والتعامل مع لهجات قواعد بيانات مختلفة، والعمل مع كائنات ResultSet الخام بدلًا من كائنات الكيانات المكتوبة التي تديرها Hibernate بالفعل. يحلّ JPQL (لغة استعلام Jakarta Persistence) هذه المشكلة بأن يتيح لك الاستعلام عن نموذج الكائنات مباشرةً. الفكرة الجوهرية هي: يعمل JPQL على الكيانات وحقولها المُعيَّنة، لا على جداول قاعدة البيانات وأعمدتها.

JPQL مقابل SQL: التحوّل المفاهيمي

حين تكتب استعلام SQL أصليًا فأنت تخاطب قاعدة البيانات الفيزيائية: أسماء الجداول والأعمدة وشروط الربط بين المفاتيح الخارجية. حين تكتب JPQL فأنت تخاطب نموذج نطاق Java: أسماء فئات الكيانات وأسماء الحقول والعلاقات التي أعلنتها بـ @OneToMany و@ManyToOne وما شابه ذلك.

لنفترض وجود كيان Product مُعيَّن على الجدول products، بحقل unitPrice مُعيَّن على العمود unit_price. في SQL تكتب:

-- SQL: يستخدم أسماء الجداول والأعمدة الفيزيائية SELECT * FROM products WHERE unit_price > 50;

أما في JPQL فتكتب:

-- JPQL: يستخدم اسم فئة الكيان واسم الحقل SELECT p FROM Product p WHERE p.unitPrice > 50

تُترجم Hibernate سلسلة JPQL هذه إلى SQL مناسبة لأي قاعدة بيانات تستهدفها — MySQL أو PostgreSQL أو Oracle أو H2 — فيبقى كود الاستعلام محمولًا بين قواعد البيانات المختلفة.

اسم الكيان وليس اسم الجدول. يكون اسم الكيان افتراضيًا هو الاسم البسيط للفئة (Product وليس products). يمكنك تخصيصه بـ @Entity(name = "Prod")، لكن معظم الفرق تتركه باسم الفئة. معرّفات JPQL حسّاسة لحالة الأحرف في أسماء الكيانات والحقول، لكنها غير حسّاسة للكلمات المفتاحية مثل SELECT وFROM وWHERE.

تشريح جملة JPQL

تحتوي جملة SELECT في JPQL على نفس البنود المنطقية في SQL لكن على الكيانات:

  • SELECT — ما تريد إسقاطه (الكيان بأكمله أو حقل بعينه أو تعبير constructor)
  • FROM — الكيان المُستعلَم عنه مع متغير تعريفي (اسم مستعار)
  • WHERE — شروط التصفية على حقول الكيان وعلاقاته
  • ORDER BY — الترتيب حسب اسم الحقل
  • GROUP BY / HAVING — التجميع (يُغطَّى في الدرس الثالث)

أول استعلام JPQL في Spring Boot 3

في مشروع Spring Boot 3 / Hibernate 6 يحصل المكوّن على EntityManager مُحقونًا من الحاوية. إليك الإعداد الأدنى — كيان Product وطريقة مستودع تُشغّل استعلام JPQL:

// Product.java package com.example.store.entity; import jakarta.persistence.*; import java.math.BigDecimal; @Entity @Table(name = "products") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; @Column(name = "unit_price", nullable = false) private BigDecimal unitPrice; @Column(nullable = false) private Integer stock; // getters / setters حُذفت للإيجاز }
// ProductRepository.java package com.example.store.repository; import com.example.store.entity.Product; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.TypedQuery; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.List; @Repository public class ProductRepository { @PersistenceContext private EntityManager em; @Transactional(readOnly = true) public List<Product> findExpensive(BigDecimal minPrice) { String jpql = "SELECT p FROM Product p WHERE p.unitPrice > :minPrice ORDER BY p.unitPrice DESC"; TypedQuery<Product> query = em.createQuery(jpql, Product.class); query.setParameter("minPrice", minPrice); return query.getResultList(); } }

لاحظ أمرين:

  1. يُعيد em.createQuery(jpql, Product.class) كائن TypedQuery<Product>، ما يعني أن getResultList() تُعيد List<Product> بلا حاجة لتحويل صريح.
  2. المعامل المُسمّى :minPrice يُربط بـ setParameter. لا تدمج أبدًا مدخلات المستخدم في سلسلة JPQL — ذلك حقن JPQL، مماثل لحقن SQL. تُغطَّى المعاملات بعمق في الدرس الرابع.

إسقاط كيان كامل مقابل حقول فردية

اختيار الكيان بأكمله (SELECT p FROM Product p) مريح لكنه يحمّل كل عمود مُعيَّن. إن كنت تحتاج فقط الاسم والسعر لصفحة قائمة الأسعار، فاسقط تلك الحقول فحسب:

// يُعيد List<Object[]> — كل مصفوفة هي [String name, BigDecimal unitPrice] TypedQuery<Object[]> q = em.createQuery( "SELECT p.name, p.unitPrice FROM Product p WHERE p.stock > 0", Object[].class ); List<Object[]> rows = q.getResultList(); for (Object[] row : rows) { String name = (String) row[0]; BigDecimal price = (BigDecimal) row[1]; System.out.printf("%s: $%.2f%n", name, price); }
افضّل إسقاطات DTO في العروض للقراءة فقط. اختيار حقول فردية يُعيد Object[] وهو هشّ (تحويل خاطئ = ClassCastException في وقت التشغيل). في الدرس التاسع ستستخدم تعبيرات constructor — SELECT NEW com.example.store.dto.PriceDto(p.name, p.unitPrice) FROM Product p — للحصول على List<PriceDto> آمن النوع. استخدم اختيار الكيان الكامل فقط حين تنوي تعديل الكائنات المُعادة ضمن نفس المعاملة.

ترقيم الصفحات بـ setFirstResult و setMaxResults

تحميل آلاف الصفوف في استعلام واحد خطأ أداء شائع. استخدم ترقيم الصفحات لاسترجاع صفحة في كل مرة — يُفوّض JPQL ذلك تلقائيًا إلى صياغة LIMIT / OFFSET الأصلية في قاعدة البيانات:

@Transactional(readOnly = true) public List<Product> findPage(int page, int pageSize) { TypedQuery<Product> query = em.createQuery( "SELECT p FROM Product p ORDER BY p.id ASC", Product.class ); query.setFirstResult(page * pageSize); // إزاحة تبدأ من 0 query.setMaxResults(pageSize); return query.getResultList(); }
حدّد ORDER BY دائمًا عند ترقيم الصفحات. بدون بند ORDER BY قد تُعيد قاعدة البيانات الصفوف بأي ترتيب، ما يعني أن صفحات مختلفة قد تُعيد نفس الصف أو تتخطى صفوفًا. اجعل عمود الترتيب ثابتًا وفريدًا قدر الإمكان — المفتاح الأساسي خيار آمن افتراضيًا.

حساسية حالة الأحرف وقواعد التسمية

لـ JPQL قواعد تسمية مهمة قد تُربك المطورين القادمين من SQL:

  • أسماء الكيانات حسّاسة لحالة الأحرف — سيُلقي FROM product استثناء IllegalArgumentException إن كانت الفئة مُسمّاة Product.
  • أسماء الحقول حسّاسة لحالة الأحرف — استخدم اسم حقل Java (unitPrice) لا اسم العمود (unit_price).
  • الكلمات المفتاحية غير حسّاسة لحالة الأحرفselect وSELECT وSelect كلها تعمل، لكن الأحرف الكبيرة هي الاصطلاح.
  • القيم الحرفية النصية تستخدم علامة الاقتباس المفردة: WHERE p.status = \'ACTIVE\'.

كيف تُترجم Hibernate JPQL إلى SQL

حين يُستدعى em.createQuery(...)، يُحلّل محلل استعلام Hibernate سلسلة JPQL إلى شجرة AST داخلية، ثم يُولّد SQL مستهدفًا اللهجة المُهيَّأة لديك. يمكنك رؤية SQL المُولَّدة — وهو أمر لا غنى عنه في تنقيح الأخطاء — بإضافة هذا إلى application.properties:

# application.properties spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.orm.jdbc.bind=TRACE

بهذه الإعدادات، يُولّد استعلام SELECT p FROM Product p WHERE p.unitPrice > :minPrice على PostgreSQL شيئًا كالتالي:

SELECT p1_0.id, p1_0.name, p1_0.unit_price, p1_0.stock FROM products p1_0 WHERE p1_0.unit_price > ?

لاحظ كيف عيَّنت Hibernate كلًّا من Product إلى products وunitPrice إلى unit_price، واستبدلت المعامل المُسمّى :minPrice بعنصر تعبئة JDBC ? — منحك بذلك استعلامًا مُعلَّم بالكامل وآمنًا من الحقن.

الخلاصة

JPQL لغة استعلام موجّهة للكائنات تعمل على نموذج كيانك لا على جداول قاعدة البيانات. تكتب الاستعلامات بمصطلحات أسماء الفئات والحقول، وتُترجمها Hibernate إلى SQL خاصة بلهجة قاعدة البيانات في وقت التشغيل. استخدم TypedQuery لتجنب التحويل الصريح، واستخدم دائمًا المعاملات ولا تدمج النصوص مطلقًا، وأضف ORDER BY عند ترقيم الصفحات، وفعّل تسجيل SQL أثناء التطوير لفهم ما يصل فعليًا إلى قاعدة البيانات. يمتد الدرس التالي هذا الأساس بانضمامات JPQL وانضمامات الجلب للتنقل في علاقات الكيانات.