الإسقاطات واستعلامات DTO
الإسقاطات واستعلامات DTO
حين تُنفّذ استعلام JPQL يُعيد كيانًا مُدارًا، يُحمّل Hibernate كل عمود مُعيَّن، ويُسجّل الكائن في ذاكرة التخزين المؤقت من المستوى الأول، ويتتبّعه للكشف عن التعديلات. بالنسبة لعمليات القراءة الإبلاغية وردود واجهات API التي تحتاج إلى حقول قليلة فحسب، هذا التكلف مجرد هدر. تُتيح لك الإسقاطات (Projections) اختيار الأعمدة التي تحتاجها تحديدًا وتحويلها مباشرةً إلى كائن DTO خفيف الوزن — متجاوزًا تمامًا تكاليف الكيانات.
لماذا تتجنب تحميل الكيانات الكاملة للبيانات للقراءة فقط
تخيّل نقطة نهاية لكتالوج المنتجات تُعيد معرّف ومقبوض واسم وسعر لكل منتج. تحميل كيان Product كاملًا يجلب أيضًا وصف الكتلة وروابط الصور والطوابع الزمنية للتدقيق وأي علاقات محمّلة مسبقًا. كل واحدة من هذه تصبح كائنًا على الكومة. تحت الحمل مع مئات الطلبات المتزامنة، هذا الفارق قابل للقياس: ضغط أكبر على جمع القمامة، وإخفاقات أكبر في ذاكرة التخزين المؤقت L1، وتنفيذ استعلام أبطأ لأن قاعدة البيانات ترسل مزيدًا من البايتات عبر الشبكة.
الإسقاطات القياسية مع مصفوفات Object
أبسط إسقاط هو تحديد الحقول المُسمّاة وإعادة مصفوفات Object[]:
يعمل هذا لكنه هشّ: التعيين من الفهرس إلى الحقل ضمني ويكسر بصمت إذا أُعيد ترتيب عبارة SELECT. استخدمه فقط للنماذج الأولية السريعة.
تعبيرات المنشئ — الأسلوب المعياري
يدعم JPQL تعبير NEW الذي يستدعي منشئ Java مباشرةً داخل الاستعلام:
يقرأ Hibernate اسم الفئة المؤهَّل بالكامل، ويحل المنشئ المطابق، ويستدعيه لكل صف. النتيجة قائمة ذات نوع صارم بلا تحويل نوع صريح.
NEW في JPQL. السجلات غير قابلة للتغيير وموجزة وتعمل بشكل مثالي لنماذج القراءة.
إسقاطات الواجهة في Spring Data JPA
يُضيف Spring Data JPA آلية إسقاط على مستوى أعلى: تُعرّف واجهة بأساليب getter تُطابق الحقول التي تريدها، ويُنشئ Spring وكيلًا في وقت التشغيل.
getCategoryName() التي تفوّض إلى product.getCategory().getName())، سيُطلق Spring استعلامًا منفصلًا لكل صف. تحقق دائمًا من SQL المُولَّد بواسطة spring.jpa.show-sql=true عند استخدام إسقاطات متداخلة.
إسقاطات الفئة (DTO) مع @Query
للنهج الأكثر وضوحًا وأمانًا أمام إعادة البناء، اجمع @Query مع تعبير المنشئ:
لاحظ أن lineTotal() يُحسب في Java من الحقول المُحمَّلة بالفعل — لا حاجة لاستعلام إضافي. هذا نمط شائع: اجلب الأرقام الخام، واحسب القيم المشتقة في DTO.
إسقاطات DTO في Criteria API باستخدام CriteriaBuilder.construct()
حين يُبنى الاستعلام ديناميكيًا (الدرس 6)، يمكنك الإسقاط في DTO باستخدام CriteriaBuilder.construct():
هذا هو المكافئ الآمن للنوع لـ NEW في JPQL، وقابل للاستخدام مع المحددات المبنية ديناميكيًا.
اختيار استراتيجية الإسقاط الصحيحة
- تحميل الكيان الكامل — حين تحتاج إلى تعديل البيانات وتثبيت التغييرات. يوفّر لك الكشف عن التعديلات في سياق الثبات جمل UPDATE الصريحة.
- تعبير المنشئ / سجل DTO — الافتراضي لنقاط النهاية للقراءة فقط. مكتوب بنوع صارم، بلا تكاليف إضافية، محمول عبر موفّري JPA.
- إسقاط الواجهة — مناسب حين تستخدم استعلامات Spring Data المشتقة والإسقاط ضحل (بلا اجتياز علاقات). كود أقل لكن به المزيد من الخفاء.
- مصفوفة Object[] القياسية — تجنّبها في كود الإنتاج؛ مفيدة للتصحيح المؤقت.
مقارنة الأداء
تخيّل استعلامًا يُعيد 1,000 طلبية بخمسة أعمدة لكل منها. قد يُطلق تحميل كيانات Order الكاملة وكلاء التحميل الكسول لعلاقة Customer، مما يُنتج 1,001 جملة SQL (N+1 الكلاسيكية). يُصدر إسقاط DTO مع JOIN في استعلام JPQL جملة واحدة بالضبط وينقل فقط الأعمدة الخمسة المطلوبة — أسرع بـ 3 إلى 10 أضعاف عادةً لأحمال الإبلاغ.
hibernate.generate_statistics=true) أو P6Spy لحساب جمل SQL الفعلية في اختبارات التكامل قبل التبديل إلى الإسقاطات وبعده.
الخلاصة
الإسقاطات هي الأداة الأساسية للحفاظ على مسارات القراءة خفيفة في تطبيق JPA. تعبير المنشئ (NEW com.example.dto.SomeDto(...)) هو الاختيار المحمول والصريح ويتزاوج تمامًا مع سجلات Java. تُقلّل إسقاطات واجهة Spring Data الكود المتكرر للحالات البسيطة لكنها تستلزم الحذر حول اجتياز العلاقات. تُمدّد إسقاطات Criteria API عبر cb.construct() نفس النمط للاستعلامات الديناميكية. في جميع الحالات الهدف واحد: احمل فقط البيانات التي تحتاجها، وتجاوز تكاليف سياق الثبات، وأعد كائن قيمة عاديًا وغير قابل للتغيير لمستدعييك.