From 895910a53f661808aa88e69f7dba7016ac1d494b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hor=C3=A1nszki=20Patrik?= Date: Mon, 12 Jan 2026 22:50:28 +0100 Subject: [PATCH] EX-235: Implement QueryDSL for dynamic database queries --- backend/exence/build.gradle.kts | 14 ++++ backend/exence/gradle/libs.versions.toml | 3 + .../impl/TransactionControllerImpl.java | 6 +- .../TransactionPredicateBuilder.java | 82 +++++++++++++++++++ .../repository/TransactionRepository.java | 21 +---- .../service/impl/TransactionServiceImpl.java | 15 ++-- 6 files changed, 112 insertions(+), 29 deletions(-) create mode 100644 backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/TransactionPredicateBuilder.java diff --git a/backend/exence/build.gradle.kts b/backend/exence/build.gradle.kts index 35e511d..d7d20d8 100644 --- a/backend/exence/build.gradle.kts +++ b/backend/exence/build.gradle.kts @@ -61,6 +61,20 @@ dependencies { implementation(libs.liquibase.core) runtimeOnly(libs.postgresql) + // QueryDSL + implementation(libs.querydsl.jpa) { + artifact { + classifier = "jakarta" + } + } + annotationProcessor(libs.querydsl.apt) { + artifact { + classifier = "jakarta" + } + } + annotationProcessor("jakarta.annotation:jakarta.annotation-api") + annotationProcessor("jakarta.persistence:jakarta.persistence-api") + // Development tools developmentOnly(libs.spring.boot.devtools) diff --git a/backend/exence/gradle/libs.versions.toml b/backend/exence/gradle/libs.versions.toml index 253c72b..59d066a 100644 --- a/backend/exence/gradle/libs.versions.toml +++ b/backend/exence/gradle/libs.versions.toml @@ -13,6 +13,7 @@ emoji-java = "5.1.1" postgresql = "42.7.4" mockito = "5.5.0" bouncycastle = "1.70" +querydsl = "5.1.0" [libraries] spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa" } @@ -39,6 +40,8 @@ emoji-java = { module = "com.vdurmont:emoji-java", version.ref = "emoji-java" } postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } bouncycastle = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bouncycastle" } +querydsl-jpa = { module = "com.querydsl:querydsl-jpa", version.ref = "querydsl" } +querydsl-apt = { module = "com.querydsl:querydsl-apt", version.ref = "querydsl" } [plugins] spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } diff --git a/backend/exence/src/main/java/com/exence/finance/modules/transaction/controller/impl/TransactionControllerImpl.java b/backend/exence/src/main/java/com/exence/finance/modules/transaction/controller/impl/TransactionControllerImpl.java index df23300..f8c082d 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/transaction/controller/impl/TransactionControllerImpl.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/transaction/controller/impl/TransactionControllerImpl.java @@ -13,6 +13,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; @@ -40,9 +41,12 @@ public ResponseEntity getTransactionById(@PathVariable("id") Lon return ResponseFactory.ok(transactionDTO); } + // TODO: EX-241: Pageable annotation refactor @GetMapping() public ResponseEntity> getTransactions(@Valid @ModelAttribute TransactionFilter filter, - @PageableDefault(size = 20) Pageable pageable) { + @PageableDefault(sort = "date", + direction = Sort.Direction.DESC, + size = 20) Pageable pageable) { Page page = transactionService.getTransactions(filter, pageable); return ResponseFactory.page(page); } diff --git a/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/TransactionPredicateBuilder.java b/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/TransactionPredicateBuilder.java new file mode 100644 index 0000000..a74a147 --- /dev/null +++ b/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/TransactionPredicateBuilder.java @@ -0,0 +1,82 @@ +package com.exence.finance.modules.transaction.repository; + +import com.exence.finance.modules.transaction.dto.TransactionType; +import com.exence.finance.modules.transaction.dto.request.TransactionFilter; +import com.exence.finance.modules.transaction.entity.QTransaction; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Predicate; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.time.Instant; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class TransactionPredicateBuilder { + + private static final QTransaction qTransaction = QTransaction.transaction; + + public static Predicate buildPredicate(TransactionFilter filter) { + if (filter == null) { + return null; + } + + BooleanBuilder builder = new BooleanBuilder(); + + addKeywordFilter(builder, filter.getKeyword()); + addCategoryFilter(builder, filter.getCategoryId()); + addTypeFilter(builder, filter.getType()); + addDateRangeFilter(builder, filter.getDateFrom(), filter.getDateTo()); + addAmountRangeFilter(builder, filter.getAmountFrom(), filter.getAmountTo()); + addRecurringFilter(builder, filter.getRecurring()); + + return builder.hasValue() ? builder : null; + } + + private static void addKeywordFilter(BooleanBuilder builder, String keyword) { + if (StringUtils.hasText(keyword)) { + String lowerKeyword = keyword.toLowerCase(); + builder.and( + qTransaction.title.lower().contains(lowerKeyword) + .or(qTransaction.note.lower().contains(lowerKeyword)) + ); + } + } + + private static void addCategoryFilter(BooleanBuilder builder, Long categoryId) { + if (categoryId != null) { + builder.and(qTransaction.category.id.eq(categoryId)); + } + } + + private static void addTypeFilter(BooleanBuilder builder, TransactionType type) { + if (type != null) { + builder.and(qTransaction.type.eq(type)); + } + } + + private static void addDateRangeFilter(BooleanBuilder builder, Instant dateFrom, Instant dateTo) { + if (dateFrom != null) { + builder.and(qTransaction.date.goe(dateFrom)); + } + if (dateTo != null) { + builder.and(qTransaction.date.loe(dateTo)); + } + } + + private static void addAmountRangeFilter(BooleanBuilder builder, BigDecimal amountFrom, BigDecimal amountTo) { + if (amountFrom != null) { + builder.and(qTransaction.amount.goe(amountFrom)); + } + if (amountTo != null) { + builder.and(qTransaction.amount.loe(amountTo)); + } + } + + private static void addRecurringFilter(BooleanBuilder builder, Boolean recurring) { + if (recurring != null) { + builder.and(qTransaction.recurring.eq(recurring)); + } + } +} diff --git a/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/TransactionRepository.java b/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/TransactionRepository.java index 55fe847..51cb146 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/TransactionRepository.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/transaction/repository/TransactionRepository.java @@ -1,12 +1,12 @@ package com.exence.finance.modules.transaction.repository; import com.exence.finance.modules.transaction.dto.TransactionType; -import com.exence.finance.modules.transaction.dto.request.TransactionFilter; import com.exence.finance.modules.transaction.entity.Transaction; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -14,28 +14,11 @@ import java.util.Optional; @Repository -public interface TransactionRepository extends JpaRepository { +public interface TransactionRepository extends JpaRepository, QuerydslPredicateExecutor { @Query("SELECT t FROM Transaction t WHERE t.id = :id") Optional find(@Param("id") Long id); - @Query("SELECT t FROM Transaction t WHERE " + - "(:#{#filter.keyword} IS NULL OR :#{#filter.keyword} = '' OR " + - "LOWER(t.title) LIKE %:#{#filter.keyword?.toLowerCase()}% OR " + - "LOWER(t.note) LIKE %:#{#filter.keyword?.toLowerCase()}%) " + - "AND (:#{#filter.categoryId} IS NULL OR t.category.id = :#{#filter.categoryId}) " + - "AND (:#{#filter.type} IS NULL OR t.type = :#{#filter.type}) " + - "AND (COALESCE(:#{#filter.dateFrom}, t.date) IS NULL OR t.date >= COALESCE(:#{#filter.dateFrom}, t.date)) " + - "AND (COALESCE(:#{#filter.dateTo}, t.date) IS NULL OR t.date <= COALESCE(:#{#filter.dateTo}, t.date)) " + - "AND (:#{#filter.amountFrom} IS NULL OR t.amount >= :#{#filter.amountFrom}) " + - "AND (:#{#filter.amountTo} IS NULL OR t.amount <= :#{#filter.amountTo}) " + - "AND (:#{#filter.recurring} IS NULL OR t.recurring = :#{#filter.recurring}) " + - "ORDER BY t.date DESC") - Page findWithFilter( - @Param("filter") TransactionFilter filter, - Pageable pageable - ); - @Query("SELECT t FROM Transaction t WHERE " + "t.recurring = true " + "AND t.type = :type " + diff --git a/backend/exence/src/main/java/com/exence/finance/modules/transaction/service/impl/TransactionServiceImpl.java b/backend/exence/src/main/java/com/exence/finance/modules/transaction/service/impl/TransactionServiceImpl.java index 386f8c4..f105e47 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/transaction/service/impl/TransactionServiceImpl.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/transaction/service/impl/TransactionServiceImpl.java @@ -14,8 +14,10 @@ import com.exence.finance.modules.transaction.dto.response.TransactionTotalsResponse; import com.exence.finance.modules.transaction.entity.Transaction; import com.exence.finance.modules.transaction.mapper.TransactionMapper; +import com.exence.finance.modules.transaction.repository.TransactionPredicateBuilder; import com.exence.finance.modules.transaction.repository.TransactionRepository; import com.exence.finance.modules.transaction.service.TransactionService; +import com.querydsl.core.types.Predicate; import java.math.BigDecimal; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -44,17 +46,12 @@ public TransactionDTO getTransactionById(Long id) { public Page getTransactions(TransactionFilter filter, Pageable pageable){ Page transactions; + Predicate predicate = TransactionPredicateBuilder.buildPredicate(filter); - if (filter == null || filter.hasActiveFilter()) { - transactions = transactionRepository.findWithFilter(filter, pageable); + if (predicate != null) { + transactions = transactionRepository.findAll(predicate, pageable); } else { - Sort sortByDateDesc = Sort.by(Sort.Direction.DESC, "date"); - Pageable sortedByDate = PageRequest.of( - pageable.getPageNumber(), - pageable.getPageSize(), - pageable.getSort().and(sortByDateDesc) - ); - transactions = transactionRepository.findAll(sortedByDate); + transactions = transactionRepository.findAll(pageable); } return transactions.map(transactionMapper::mapToTransactionDTO);