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

الميتاموديل والأمان النوعي

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

الميتاموديل والأمان النوعي

في الدرس السابق بنيت قوائم Predicate ديناميكية باستخدام Criteria API مع نصوص خامّة مثل root.get("price"). يعمل ذلك — لكنه يتعطل وقت التشغيل لا وقت الترجمة عند خطأ في كتابة اسم الحقل أو عند تغييره أثناء إعادة البناء. ميتاموديل JPA الثابت هو الحل: يُنشئ فئةً مرافقة لكل كيان تعرض كل خاصية مستمرة كثابت ذي نوع صارم، محوّلًا مفاجآت وقت التشغيل إلى أخطاء وقت الترجمة.

ما هو الميتاموديل الثابت

تُعرّف مواصفة JPA (§6.2) الميتاموديل الثابت بأنه مجموعة فئات مُوَلَّدة، واحدة لكل كيان، تعيش في نفس الحزمة وتحمل اسم الكيان مع شرطة سفلية لاحقة. للكيان Product في الحزمة com.shop.domain، فئة الميتاموديل هي com.shop.domain.Product_. تحتوي على حقل ثابت عام لكل خاصية مستمرة:

// مُوَلَّد تلقائيًا — لا تحرّره يدويًا package com.shop.domain; import jakarta.persistence.metamodel.SingularAttribute; import jakarta.persistence.metamodel.ListAttribute; import jakarta.persistence.metamodel.StaticMetamodel; @StaticMetamodel(Product.class) public abstract class Product_ { public static volatile SingularAttribute<Product, Long> id; public static volatile SingularAttribute<Product, String> name; public static volatile SingularAttribute<Product, Double> price; public static volatile SingularAttribute<Product, Boolean> active; public static volatile SingularAttribute<Product, Category> category; public static volatile ListAttribute<Product, OrderLine> orderLines; }

الحقول مُكتَّبة بأنواع الميتاموديل الخاصة بـ JPA: SingularAttribute<Owner, FieldType> وListAttribute<Owner, ElementType> وSetAttribute وMapAttribute وغيرها. تُمرّرها مباشرةً إلى توابع Criteria API التي تقبل SingularAttribute — دون الحاجة لأي نصوص.

توليد الميتاموديل

يُنتَج الميتاموديل بواسطة معالج تعليقات JPA وقت الترجمة. مع Hibernate 6 وMaven أضف المعالج إلى ملف البناء:

<!-- pom.xml --> <dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-jpamodelgen</artifactId> <version>6.4.4.Final</version> <scope>provided</scope> <!-- للترجمة فقط، لا يُحزَّم --> </dependency>

مع Spring Boot وGradle:

// build.gradle (Groovy DSL) dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' annotationProcessor 'org.hibernate.orm:hibernate-jpamodelgen:6.4.4.Final' }

بعد البناء (mvn compile أو gradle compileJava) تظهر فئات *_ في target/generated-sources/annotations (Maven) أو build/generated/sources/annotationProcessor (Gradle). يحتاج IDE الخاص بك لإدراج هذه المجلدات كجذور مصدر — يفعل IntelliJ IDEA ذلك تلقائيًا عند وجود معالج التعليقات في مسار ترجمة.

لا حاجة لتغيير الكيان. لا تُضيف أي شيء إلى فئة الكيان. يقرأ معالج التعليقات @Entity و@Column و@OneToMany وغيرها من الـ bytecode ويُولّد فئة الميتاموديل تلقائيًا. أعد تشغيل البناء عند إضافة أي حقل أو إعادة تسميته.

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

قارن الأسلوبين جنبًا إلى جنب. بدون الميتاموديل:

// مبني على نصوص — لا أمان وقت الترجمة Root<Product> root = cq.from(Product.class); Predicate cheap = cb.lessThan(root.<Double>get("price"), 100.0); // خطأ إملائي؟ يُكتشف وقت التشغيل

مع الميتاموديل:

import com.shop.domain.Product_; import jakarta.persistence.*; import jakarta.persistence.criteria.*; // داخل مستودع أو خدمة public List<Product> findCheapActive(double maxPrice) { CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Product> cq = cb.createQuery(Product.class); Root<Product> root = cq.from(Product.class); Predicate pricePred = cb.lessThan(root.get(Product_.price), maxPrice); Predicate activePred = cb.isTrue(root.get(Product_.active)); cq.select(root) .where(cb.and(pricePred, activePred)) .orderBy(cb.asc(root.get(Product_.name))); return em.createQuery(cq).getResultList(); }

Product_.price هو SingularAttribute<Product, Double>. يتحقق المترجم من أن cb.lessThan يستقبل مسار Double وقيمة Double — سيرفض الترجمة عند خلط الأنواع.

الانضمامات الآمنة نوعيًا

تستفيد الانضمامات أكثر من غيرها. الحمولة الزائدة لـ Root.join() المبنية على الميتاموديل ترجع Join<Product, Category> بالأنواع العامة الصحيحة، فتستطيع التنقل إلى الكيان المنضم بفحص نوعي كامل:

import com.shop.domain.Product_; import com.shop.domain.Category_; Root<Product> product = cq.from(Product.class); Join<Product, Category> cat = product.join(Product_.category, JoinType.INNER); // Category_.name هو SingularAttribute<Category, String> Predicate catPred = cb.equal(cat.get(Category_.name), "Electronics"); cq.select(product).where(catPred);

لو أعدت تسمية حقل name في Category إلى displayName لاحقًا، يُعاد توليد الميتاموديل ولا يعود Category_.name موجودًا — يفشل البناء فورًا مشيرًا إلى كل استعلام يحتاج تحديثًا.

الاستعلامات الديناميكية مع الميتاموديل

المكسب الحقيقي للميتاموديل هو بناء توابع تصفية ديناميكية حيث تكون الخاصية نفسها معاملًا. يتيح هذا النمط كتابة مساعد عام واحد يعمل مع أي خاصية لأي كيان:

public <E, F extends Comparable<F>> List<E> findByRange( Class<E> entityClass, SingularAttribute<E, F> attribute, F from, F to) { CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<E> cq = cb.createQuery(entityClass); Root<E> root = cq.from(entityClass); cq.select(root).where( cb.between(root.get(attribute), from, to) ); return em.createQuery(cq).getResultList(); } // الاستخدام — أمان نوعي كامل في موقع الاستدعاء List<Product> midRange = findByRange(Product.class, Product_.price, 50.0, 200.0); List<Order> recent = findByRange(Order.class, Order_.placedAt, LocalDateTime.now().minusDays(7), LocalDateTime.now());
ادمجه مع Spring Data Specifications. تُغلّف واجهة Specification<T> في Spring Data JPA بانيَ محمول Criteria. استخدام الميتاموديل داخل تطبيقات Specification يمنحك قابلية التركيب التي توفرها المواصفات وأمان الميتاموديل وقت الترجمة معًا — المزيج المثالي لواجهات برمجية للتصفية والبحث في الإنتاج.

الميتاموديل الديناميكي

توفر JPA أيضًا ميتاموديلًا ديناميكيًا يمكن الوصول إليه وقت التشغيل عبر EntityManager.getMetamodel(). يفيد هذا للأطر العامة أو أدوات الإدارة التي يجب أن تفحص خصائص الكيانات دون معرفة فئة الكيان وقت الترجمة:

Metamodel mm = em.getMetamodel(); EntityType<Product> productType = mm.entity(Product.class); // استرجاع خاصية بالاسم — وقت التشغيل، لكنه مكتَّب SingularAttribute<? super Product, ?> attr = productType.getSingularAttribute("price"); System.out.println(attr.getJavaType()); // class java.lang.Double

للاستعلامات العادية في التطبيق، فضّل الميتاموديل الثابت. الديناميكي أداة تشخيص وبناء أطر عمل.

ملاحظات الأداء

الميتاموديل نفسه لا يضيف أي عبء وقت التشغيل — يُملأ نمط *_ بواسطة مزوّد JPA أثناء تهيئة EntityManagerFactory (مرة واحدة عند بدء التطبيق). استخدام Product_.price بدلًا من "price" في استعلام Criteria يُنتج SQL متطابق. الفائدة كلها على مستوى المطوّر: أخطاء أقل وإعادة بناء أكثر أمانًا ودعم IDE أفضل (البحث عن الاستخدامات، إعادة تسمية الحقول).

حافظ على تزامن الملفات المُولَّدة. إذا أعدت تسمية حقل في كيان ونسيت إعادة البناء قبل الحفظ في المستودع، سيظل الملف القديم *_ يُشير للاسم القديم. الحل بسيط: أضف مجلد target/generated-sources (Maven) أو build/generated (Gradle) إلى .gitignore حتى لا تُحفظ الملفات المُولَّدة أبدًا، واعتمد على البناء لتوليدها من جديد على كل جهاز.

الخلاصة

يردم ميتاموديل JPA الثابت الفجوة بين مرونة استعلامات Criteria الديناميكية وأمان لغة ذات أنواع ثابتة. أضف hibernate-jpamodelgen كمعالج تعليقات، أعد البناء، ويحصل كل كيان على فئة مرافقة Entity_. استبدل كل نص خام في كود Criteria بالثابت المقابل Entity_.attribute. يفرض المترجم حينئذٍ أسماء الخصائص وأنواعها، ويستطيع IDE التنقل فيها وإعادة تسميتها، وتصمد استعلاماتك أمام إعادة البناء دون إخفاء الأخطاء حتى وقت التشغيل.