التجميعات والتقسيم بالمجموعات في JPQL
التجميعات والتقسيم بالمجموعات في JPQL
نادرًا ما تعرض التطبيقات الحقيقية صفوفًا خامًا. إنها تحتاج إلى مجاميع ومتوسطات وملخصات مرتبة — "كم طلبًا لكل عميل"، "ما متوسط تقييم المنتج"، "أي فئة تحقق أعلى إيراد". تتيح JPQL دوال التجميع ذاتها الموجودة في SQL، غير أنها تُطبَّق على نموذج الكيانات لا على الجداول الخام. يتناول هذا الدرس كل دالة تجميع وGROUP BY وHAVING مع استعراض مقايضات الأداء الجوهرية في بيئات الإنتاج.
دوال التجميع الخمس
تدعم JPQL المجموعة ذاتها الموجودة في SQL تمامًا:
COUNT(x)— عدد القيم غير الفارغة؛ استخدمCOUNT(DISTINCT x)لإزالة التكرار.SUM(x)— المجموع الحسابي لمسار رقمي.AVG(x)— المتوسط الحسابي، ويُعيد دائمًا قيمة من نوعDouble.MIN(x)/MAX(x)— الأصغر والأكبر قيمة؛ تعمل على الأرقام والنصوص والتواريخ.
بدون GROUP BY تُجمّع هذه الدوال مجموعة النتائج بأكملها في صف واحد — يُسمى تجميعًا قيميًا (scalar aggregate).
COUNT دائمًا Long. تُعيد SUM على حقل int/long قيمة Long، وعلى BigDecimal قيمة BigDecimal. تُعيد AVG دائمًا Double. الخطأ في النوع يتسبب في ClassCastException وقت التشغيل لا وقت الترجمة.
GROUP BY — تجميع لكل مجموعة
تُقسّم GROUP BY مجموعة النتائج إلى دلاء قبل تطبيق التجميع. كل دلو ينتج صفًا واحدًا في المخرجات. القاعدة هي ذاتها في SQL: كل مسار في SELECT غير مُغلَّف بدالة تجميع يجب أن يظهر في GROUP BY.
التجميع بحسب سمة علاقة مثل o.customer.id يتجنب ربطًا (join)، أما التجميع بالكيان ذاته مثل o.customer فيُنشئ ربطًا ضمنيًا يحوّله Hibernate إلى عمود المفتاح الخارجي. كلا الأسلوبين يعمل، لكن المسار الصريح للمفتاح الأساسي أوضح وأسرع بشكل هامشي.
HAVING — التصفية بعد التجميع
تُصفّي WHERE الصفوف المفردة قبل التجميع. تُصفّي HAVING المجموعات بعد التجميع. استخدم HAVING متى ما كان الشرط يشير إلى دالة تجميع.
WHERE. تُطبّق قاعدة البيانات WHERE قبل التجميع مما يُقلص عدد الصفوف. أما HAVING فتعمل بعد التجميع، لذا يُجبر نقل شرط غير تجميعي إليها قاعدةَ البيانات على تجميع جميع الصفوف أولًا.
دمج عدة تجميعات
يمكنك اختيار عدة تجميعات في نفس الاستعلام. نمط شائع هو "صف الملخص" الذي يعرض المجاميع والمتوسطات والأعداد لكل مجموعة في آنٍ واحد.
استخدام توقعات DTO مع التجميعات
تحويل مصفوفات Object[] يدويًا عرضة للأخطاء. تدعم JPQL تعبير المُنشئ NEW الذي يُغلّف كل صف في DTO مكتوب بدقة مباشرةً داخل الاستعلام، متجنبًا التحويل اليدوي كليًا.
الاسم الكامل للفئة مطلوب في سلسلة الاستعلام، ويجب وجود المُنشئ المطابق. هذا النمط مُفضَّل لأي نتيجة تجميع تُعرض خارج طبقة المستودع — فهو يمنح المستدعين نوعًا ثابتًا ومُسمًّى بدلًا من مصفوفة قائمة على الموضع.
COUNT(e) مقابل COUNT(e.id) مقابل COUNT(*)
لا تدعم JPQL الصيغة COUNT(*). استخدم COUNT(e) (مستعار الكيان) كبديل اصطلاحي؛ يترجمه Hibernate إلى COUNT(id) تلقائيًا. الصيغة COUNT(e.id) مكافئة وأكثر وضوحًا. استخدم COUNT(DISTINCT e.someField) عند الحاجة إلى عد قيم غير متكررة لحقل غير مفتاح أساسي.
معالجة القيم الفارغة في التجميعات
كما في SQL، تتجاهل تجميعات JPQL قيم NULL بصمت. تُعيد SUM على عمود كل قيمه فارغة NULL لا صفرًا، وتتخطى COUNT(field) القيم الفارغة بينما تُحصي COUNT(entity) كل صف بصرف النظر. نسيان ذلك يُنتج إجابات خاطئة صامتة في لوحات المعلومات.
SUM (وAVG) القيمة null لا صفرًا. استخدم دائمًا Optional أو تحقق من القيمة الفارغة عند تخزين النتيجة، أو استخدم COALESCE(SUM(x), 0) في الاستعلام لضمان إعادة قيمة غير فارغة.
اعتبارات الأداء
تُنفَّذ استعلامات التجميع بالكامل داخل محرك قاعدة البيانات — لا تُحمَّل كيانات في ذاكرة JPA الأولى، ولا تُشغَّل علاقات كسولة، ولا تحدث تكاليف تحويل كائنات. هذا يجعلها أسرع بكثير من تحميل الكيانات وتجميعها في كود Java. بعض الإرشادات للإنتاج:
- أنشئ فهارس على أعمدة GROUP BY. إذا كنت تُجمّع بـ
customer_idأوcategoryكثيرًا فيجب فهرسة هذه الأعمدة. افحص خطة الاستعلام بـEXPLAIN. - تجنب التجميع بمسارات كيان غير مفتاحية إذا استطعت استخدام عمود المفتاح الخارجي — فالتجميع بـ
o.customerقد يُولّد ربطًا بينماo.customer.idلا يفعل. - استخدم HAVING بحذر على الجداول الكبيرة — يجب على قاعدة البيانات بناء جميع المجموعات قبل التصفية. الفهرس الشامل أو الاستعلام الفرعي المُصفَّى مسبقًا قد يساعدان حين يكون شرط HAVING انتقائيًا.
- قسّم نتائج التجميع إلى صفحات — حتى الاستعلامات المُجمَّعة قد تُعيد آلاف الصفوف في الأنظمة الكبيرة. طبّق
setMaxResults/setFirstResultكما تفعل مع أي قائمة نتائج.
الخلاصة
تعمل دوال التجميع الخمس في JPQL — COUNT وSUM وAVG وMIN وMAX — تمامًا كما في SQL لكنها تعمل على مسارات الكيانات. تُقسّم GROUP BY النتيجة إلى دلاء؛ وتُصفّي HAVING تلك الدلاء بحسب شروط تجميعية. يُفضَّل استخدام تعبيرات المُنشئ لـ DTO المكتوب بدقة على مصفوفات Object[] الخام، وتحقق من null في نتائج SUM/AVG، واجعل قاعدة البيانات تُجري الحسابات — فهي دائمًا أسرع من تحميل الكيانات في الذاكرة وتجميعها بكود Java.