JPAning cheklari bilan boshlangan QueryDSL kiritilishi: sozlamalardan dinamik so'rovgacha
1. Nima uchun QueryDSL kerak edi?
Spring Data JPA ni ishlatish orqali oddiy so'rov funksiyasini juda tez ishlab chiqish mumkin. Usul nomlash qoidalari yordamida findById, findByUserIdAndStatus kabi so'rov metodlarini osonlik bilan yaratish mumkin. Dastlabki loyihalarda bunday usul orqali ko'p hollarda talablarni yetarlicha qondirish imkoniyati bo'lgan.
Ammo xizmat kengayib, ma'lumotlar aloqalari murakkablashgan sari, cheklovlar ko'zga tashlana boshladi. Ayniqsa, bir vaqtning o'zida bir nechta jadvallarni so'rov qilish kerak bo'lgan holatlarda muammolar yuzaga keldi. Masalan, buyurtma ma'lumotlari, obuna ma'lumotlari, sozlama ma'lumotlarini birgalikda so'rov qilish zarur bo'lganida JPA Repository metodlari bilan ularning ko'pini bajarish qiyin bo'lgan.
Dastlabida har bir jadvalni alohida ko'rib chiqib, Java kodida to'g'ridan-to'g'ri ma'lumotlarni birlashtirish usulida amalga oshirdik. Loyihaning tuzilishi bo'yicha bitta asosiy ma'lumotga stajni belgilash ma'lumotlari bog'langan bo'lib, ro'yxatni ko'rib chiqishda har bir ma'lumotning stajni belgilash ma'lumotlarini qo'shimcha ravishda ko'rib chiqishimiz kerak edi va natijada ko'rib chiqish soni keskin oshdi. Bu usul quyidagi hal qiluvchi muammoga ega edi.
-
DB aylanma soni (Round Trip) oshirish
-
N+1So'rov portlashiga olib keladigan muammo
-
keraksiz xotiraFoydalanishning ortishi va ko'rish logikasining murakkabligi oshishi
Dastlab, ma'lumot hajmi kichik bo'lganligi uchun katta muammolar bo'lmagan, lekin umumiy tizim ma'lumotlari ko'payib, xizmatdan foydalanish ortgan sari javob tezligi sezilarli darajada pasayib ketdi. Buni hal qilish uchun bir nechta jadvallarni bitta JOIN so'rovi orqali ko'rish tuzilmasi zarur bo'ldi.
Albatta, JPQL'ni to'g'ridan-to'g'ri ishlatish usulini ham ko'rib chiqdik. Biroq, JPQL matn asosida yozilgani uchun, typografik xatolarni kompilyatsiya paytida aniqlash mumkin emas, refaktoring davomida barqarorlik pasayadi va dinamik shartlarni qayta ishlash murakkab edi. Oxir-oqibat, turli xil dinamik so'rovlarni qulay va xavfsiz tarzda yozish uchun QueryDSL ni joriy etishga qaror qildik.
2. QueryDSL ni sozlash
Mavjud loyiha uchun QueryDSL konfiguratsiyasi mavjud emasdi, shuning uchun boshidan to'g'ridan-to'g'ri muhitni sozlashga kirishishim kerak edi. Dastlab, build.gradle'ga faqat bog'lanishlarni qo'shish kifoya, deb o'yladim, lekin aslida Spring Boot 3.x muhitiga mos keladigan batafsil sozlamalar kerak edi. Faqat bog'lanishlarni kiritsam va ishga tushirsam, JPAQueryFactory'ni topa olmadim, shuning uchun tuzilgan sozlash usuli quyidagicha.
2-1. build.gradle bog'lanishlarni qo'shish
dependencies {
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
sourceSets {
main {
java {
srcDirs = ['src/main/java',
'build/generated/sources/annotationProcessor/java/main']
}
}
}
Spring Boot 3.x muhiti eski javax emas, jakarta EE asosida bo'lganligi sababli, querydsl-jpa va querydsl-apt bog'lanishlarining orqasiga albatta :jakarta classifier qo'shishingiz kerak. Agar bu imkoniyatni o'chirib qo'ysangiz, ichki ravishda javax paketiga murojaat qilib, kompilyatsiya xatoligiga olib keladi.
Shuningdek, annotationProcessor 3 turdagi to'plam bilan birga sourceSets sozlamasini qo'shib, Gradle avtomatik ravishda yaratilgan generated katalogni manba yo'li sifatida to'g'ri tanishini ta'minlashingiz kerak. Ushbu sozlama yo'q bo'lsa, build muvaffaqiyatli bo'lsa ham IDE dan Q-sinflarini import qilish muammosi yuzaga keladi.
2-2. Qklass va JPAQueryFactory
Muvaffaqiyatli qurish amalga oshirilsa, @Entity klasslariga asoslanib QOrderJpo, QMemberJpo kabi Qklasslar avtomatik ravishda belgilangan yo'lga yaratiladi.
JPQL mavjud string usuli va QueryDSL usulini solishtirganda, turli xil xavfsizlikning farqi aniq ko'rinadi.
// 1. String-based JPQL approach: Syntax errors cannot be caught at compile time
String jpql = "SELECT o FROM Order o WHERE o.userId = :userId";
// 2. QueryDSL approach: Safe types and compiled validation
QOrderJpo order = QOrderJpo.orderJpo;
queryFactory
.selectFrom(order)
.where(order.userId.eq(userId))
.fetch();
Shunday qilib, QueryDSL Q-sinfini ishlatib, so'rovlarni Java kodida yozishga imkon beradi, shuning uchun IDE avtomatik to'ldirish va kompilyator tekshiruvi funksiyalaridan 100% foydalanishingiz mumkin va entitiy maydonlarining nomlarini o'zgartirishda juda xavfsizdir.
Bunday yaratilgan Q-sinfi asosidagi so'rovni bajarish uchun JPAQueryFactory kerak, chunki Spring avtomatik ravishda quruq registratsiya qilmaydi, shuning uchun quyida ko'rsatilgandek alohida Configuration sozlash klassini yaratib, EntityManager ni qo'shib quruq registratsiya qilishingiz kerak.
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
Sozlash tugagandan so'ng, har bir Repository dan quruvchi qo'shish (@RequiredArgsConstructor) usuli orqali JPAQueryFactory ni qo'shib, osonlik bilan foydalanishingiz mumkin.
3. Asosiy foydalanish: JOIN so'rovi
QueryDSL sozlamalari tugallangandan so'ng, birinchi hal qilingan masala murakkab ko'p jadvalli JOIN mantiqi bo'ldi. Buyurtma ma'lumotlarini ko'rish jarayonida a'zolar ma'lumotlari, kategoriyalar ma'lumotlarini birga olib kelish zaruriyati qo'llandi.
List result = queryFactory
.selectFrom(order)
.innerJoin(customer).on(order.customerId.eq(customer.id))
.innerJoin(category).on(order.categoryId.eq(category.id))
.where(
order.userId.eq(userId),
order.shopId.eq(shopId)
)
.fetch();
QueryDSLning innerJoin().on()dan foydalanish orqali bog'lanish xaritalari (FK tuzilmasi) bo'lmagan mustaqil jadvallarni to'g'ridan-to'g'ri qo'shish shartlarini belgilab, moslashuvchan ravishda birlashtirish mumkin. Shuningdek, where() qismidagi vergul (,) bilan ajratilgan bir nechta shartlar avtomatik ravishda AND shartlariga birlashtiriladi, shuning uchun o'qilishi oson bo'ladi va juda sodda ifodalanadi.
Ushbu qo'shish so'rovi tufayli avval bir necha marta kichik qismga bo'linib chaqiriladigan DB safarlarining soni atigi 1 marta birlashtirildi va xotira ustida Java kodida murakkab qilib bog'langan xaritalash mantiqiyasi yo'q bo'lib, server yuklamasi kamaydi va javob tezligi keskin yaxshilandi.
4. Muxtor: Dinamik so'rov + Sahifalash
Amaliy muhitda qidiruv funksiyasi ko'pincha kalit so'zlar, a'zo bo'lish sanasi, holat kabi turli qidiruv shartlari tanlovga keladigan 'dinamik so'rov' tarzida bo'ladi. Avvalgi JPQLda if bayonoti bilan satrlarni qo'shib, o'qilishi qiyinlashadigan holatlar ko'p bo'lar edi, ammo QueryDSL BooleanExpression yordamida Lego bloklarini yig'ish kabi toza va qulay ravishda bajarilishi mumkin.
JPAQuery query = queryFactory
.selectFrom(member)
.join(subscription).on(member.id.eq(subscription.memberId))
.where(
subscription.shopId.eq(shopId),
keywordContains(keyword), // returns null if empty, ignored by where clause
statusEq(status) // returns null if empty, ignored by where clause
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize());
JPAQuery countQuery = queryFactory
.select(member.count()).from(member)
.join(subscription).on(member.id.eq(subscription.memberId))
.where(
subscription.shopId.eq(shopId),
keywordContains(keyword),
statusEq(status)
);
return PageableExecutionUtils.getPage(query.fetch(), pageable, countQuery::fetchOne);
private BooleanExpression keywordContains(String keyword) {
if (!StringUtils.hasText(keyword)) return null;
String cleaned = keyword.toLowerCase().replace(" ", "");
StringExpression expr = Expressions.stringTemplate(
"replace({0}, ' ', '')", member.name.lower()
);
return expr.contains(cleaned);
}
private BooleanExpression statusEq(MemberStatus status) {
return status != null ? member.status.eq(status) : null;
}
-
Dinamik so'rov va optimallashtirishning asosiy nuqtalari
1. Null avtomatik olib tashlash orqali o'qish qulayligi: where()Agar kiruvchi yordamchi usul nullni qaytarsa, QueryDSL shartni avtomatik ravishda o'chiradi. Bu shartlar soni ko'payganda ham where matnining tuzilishini toza saqlashga yordam beradi.
2. PageableExecutionUtils orqali Count so'rovini optimallashtirish: pagingMa'lumotlarni qayta ishlashda har doim count so'rovini yuborish, katta hajmdagi jadvalda katta yuk bo'ladi. PageableExecutionUtils.getPage() ni ishlatganda, birinchi sahifa va ma'lumotlar umumiy sonidan kam bo'lganida yoki oxirgi sahifada keraksiz count so'rovlarini o'tkazib yuborib, amaliyot samaradorligini optimallashtirishda juda foydali.
3. Expressions.stringTemplate orqali DB funktsiyalarini boshqarish: talablarBo'sh joylarni olib tashlab, katta-kichik harflarga e'tibor bermaydigan aniq qidiruv zarur edi. Ma'lumotlarni dastur xotirasiga olib kelish o'rniga, Expressions.stringTemplate dan foydalanib, DB darajasidagi REPLACE va LOWER funksiyalarini QueryDSL ichida xavfsiz birlashtirib, murakkab matnni qayta ishlash talablarini toza tarzda hal qilish imkoniyatiga ega bo'ldim.
5. Xulosa
Boshlang'ich sozlash jarayonida Spring Boot 3.x muhitidagi Jakarta paketi o'tkazish muammolari va Gradle'ning sourceSets yo'llarini tanib olish kabi e'tibordan chetda qolishi mumkin bo'lgan nuqtalar mavjud edi, shuning uchun bir necha marta urinish va xatoliklarga duch keldim. Biroq, xatolarni to'g'rilash orqali QueryDSLning ishlash prinsipini va ichki tuzilmasini chuqurroq tushunishim uchun yaxshi imkoniyat bo'ldi.
Infratuzilma o'rnatilgandan so'ng, turli xil yozishmalarga nisbatan barqarorlikni ta'minlash, ish vaqti xatolari o'rniga kompilyatsiya xatolari bosqichida imlo xatolarini aniqlash qulayligi va dinamik so'rovlarning ajralib turadigan qulayligi kabi oddiy so'rov quruvchidan yuqoriroq qiymatini sezdim. Ayniqsa, so'rovlar markaziga qaratilgan biznes mantiqi rivojlanib borar ekan, QueryDSL Spring Data JPAning cheklovlarini to'ldiruvchi eng kuchli va muhim qurol ekani to'g'risida o'ylayman.
Qo'llanma
• QueryDSL rasmiy hujjati. http://querydsl.com/static/querydsl/5.0.0/reference/html_single/
• Spring Data JPA rasmiy hujjati. https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
• Spring Boot 3.x rasmiy hujjati – Jakarta EE Migratsiya. https://docs.spring.io/spring-boot/docs/current/reference/html/
eunice