استعلامات SQL الأصلية
استعلامات SQL الأصلية
تُغطّي JPQL وواجهة Criteria API الغالبية العظمى من احتياجات الاستعلام في تطبيقات JPA. لكن كل طبقة تجريد لها سقف، وJPA ليست استثناءً. حين تحتاج إلى دالة نافذة (window function)، أو CTE تكراري، أو تعبير بحث نصي كامل، أو تلميح خاص بقاعدة البيانات، أو عملية مجمّعة تتجاوز ORM كليًا، عليك النزول إلى SQL الأصلية. يجعل Spring Boot 3 وHibernate 6 هذا الانتقال سلسًا مع إبقاء تعيين النتائج تحت سيطرتك الكاملة.
متى تلجأ إلى SQL الأصلية
قبل كتابة استعلام أصلي، تأكد من عجز JPQL أو Criteria عن تلبية الحاجة. إن انطبق أيٌّ مما يلي، فالSQL الأصلية هي الأداة المناسبة:
- تحتاج إلى ميزة خاصة بالمورّد — مثل
DISTINCT ONفي PostgreSQL، أوGROUP_CONCATفي MySQL، أوCROSS APPLYفي SQL Server. - يستخدم الاستعلام دوال النوافذ (
ROW_NUMBER()، وRANK()، وLEAD()/LAG()) التي لا تستطيع JPQL التعبير عنها. - تحتاج إلى CTE تكراري لاجتياز شجرة أو تسلسل هرمي مخزون في جدول ذاتي المرجع.
- يجب تنفيذ استعلام قدّمه مسؤول قاعدة البيانات مع تلميحات للمحسّن (
USE INDEX،NOLOCK) حرفيًا. - عملية
UPDATEأوDELETEمجمّعة على ملايين الصفوف حيث يكون تحميل الكيانات مُكلفًا جدًا. - استعلام تقرير يربط جداول عديدة في إسقاط مسطّح — أسرع في الكتابة وأسرع في التنفيذ.
إنشاء استعلام أصلي عبر EntityManager
تعرض EntityManager الدالة createNativeQuery(sql). في أبسط صورها، تُعاد النتائج كصفوف من نوع Object[]:
كل عنصر في القائمة عبارة عن Object[] تتوافق مؤشراته مع أعمدة SELECT بالترتيب. افضّل دائمًا المعاملات المُسمّاة (:lim) على المعاملات الموضعية (?1) لمزيد من القابلية للقراءة وتجنّب أخطاء التعداد.
تعيين النتائج إلى فئة كيان
حين يختار الاستعلام الأصلي كل أعمدة جدول واحد، مرّر فئة الكيان كوسيطة ثانية لـ createNativeQuery وسيعيّن Hibernate الأعمدة إلى نسخ كيانات مُدارة تلقائيًا:
الكائنات المُعادة مُدارة بالكامل: يمكن تهيئة الارتباطات الكسولة، وتُحترم حقول @Version، وأي تعديلات داخل المعاملة ذاتها ستُنظَّف. هذه أنظف صورة للاستعلام الأصلي حين يمسّ SQL جدولًا مُعيَّنًا واحدًا.
SqlResultSetMapping — تعيين الأعمدة إلى الحقول يدويًا
حين يضمّ الاستعلام الأصلي جداول متعددة أو يستخدم أعمدة محسوبة، تحتاج إلى @SqlResultSetMapping لإخبار Hibernate بكيفية بناء النتيجة. ضعه على أي فئة كيان (اسمه عالمي):
مرّر الآن اسم التعيين كوسيطة ثالثة:
يستدعي @ConstructorResult الدالة new OrderSummaryDTO(id, fullName, total, itemCount) لكل صف. OrderSummaryDTO سجل Java أو فئة عادية — لا حاجة لأن يكون كيان JPA.
الاستعلامات الأصلية المُسمّاة
تمامًا كـ JPQL، يمكن تصريح SQL الأصلية وتجميعها مسبقًا عند بدء التشغيل باستخدام @NamedNativeQuery. هذا يُخرج SQL من سلاسل نصية داخل Java إلى موضع مركزي واضح:
الاستعلامات الأصلية في Spring Data JPA باستخدام @Query
إن كنت تستخدم مستودعات Spring Data، أضف nativeQuery = true إلى تعليقة @Query. يتولى Spring Data الباقي:
نوع الإعادة OrderSummaryProjection هو إسقاط قائم على الواجهة في Spring Data — واجهة أسماء getter فيها تطابق أسماء مستعارة الأعمدة في SELECT:
يُولّد Spring Data وكيلًا (proxy) في وقت التشغيل يقرأ الأعمدة المقابلة. هذا أكثر الأساليب إيجازًا ويتجنّب @SqlResultSetMapping كليًا.
التصفيح مع الاستعلامات الأصلية
دعم Pageable التلقائي في Spring Data لا يعمل مع الاستعلامات الأصلية لأن الإطار لا يستطيع تغليف SQL عشوائية بشكل موثوق في استعلام عدّ. قدّم countQuery صراحةً:
ذاكرة التخزين المؤقت من المستوى الأول والاستعلامات الأصلية
تفصيل جوهري في Hibernate: الاستعلامات الأصلية تتجاوز ذاكرة التخزين المؤقت من المستوى الأول (سياق الاستمرارية). إن حمّلت كيانًا ثم نفّذت UPDATE أصليًا على الصف ذاته دون flush، ستصبح حالة Hibernate في الذاكرة قديمة. نظّف EntityManager دائمًا قبل تشغيل استعلام كتابة أصلي، أو اعمل داخل معاملة جديدة:
em.clear(). إن لم تفعل، سيعكس أي كيان موجود في سياق الاستمرارية الحالةَ القديمة، مما يُفضي إلى تناقضات صامتة في البيانات داخل المعاملة ذاتها.
المفاضلات في الأداء
- لا حمل من dirty checking — الكيانات المُعادة من الاستعلامات الأصلية لا تُتابَع تلقائيًا ما لم تستخدم التحميل بفئة الكيان مع صفوف كاملة الأعمدة.
- تحكم دقيق بالSQL — يمكنك إضافة تلميحات للفهارس، وتجنّب الضمّات الضمنية التي تُنشئها ORM، واختيار الأعمدة التي تحتاجها فقط.
- تكلفة قابلية النقل — SQL الخاصة بمورّد مُعيَّن تربطك بذلك المورّد. وثّق كل استعلام أصلي مع سبب عجز JPQL عنه.
- خطر انجراف المخطط — تُشير SQL الأصلية مباشرةً إلى أسماء الجداول والأعمدة. هجرة إعادة التسمية التي تُحدّث تعليقات الكيانات لا تُحدّث سلاسل الاستعلامات الأصلية تلقائيًا.
الخلاصة
الاستعلامات الأصلية بالSQL هي مخرج الطوارئ حين تصل JPQL إلى حدودها. استخدم createNativeQuery على EntityManager مباشرةً، أو علّق دوال المستودع بـ @Query(nativeQuery = true). عيّن النتائج إلى فئات الكيانات للاستعلامات أحادية الجدول، وإلى إسقاطات قائمة على الواجهة للتجميعات متعددة الجداول، وإلى تعيينات @ConstructorResult حين تحتاج إلى أقصى قدر من التحكم. نظّف دائمًا قبل الكتابات الأصلية وامسح الذاكرة المؤقتة بعدها، ووثّق سبب وجود كل استعلام أصلي حتى يفهم المطوّرون المستقبليون النيّة من ورائه.