From 8574e8fdf98a7d2bfaf1246a90408b21265bbd09 Mon Sep 17 00:00:00 2001 From: Thuan ngo Date: Fri, 4 Jul 2025 20:43:56 +0700 Subject: [PATCH] fix CourseRepository findByMultiquery --- .../course/CourseRepositoryCustomImpl.java | 158 ++++++++++++++++++ .../domain/course/CourseServiceImpl.java | 12 +- .../domain/student/StudentServiceImpl.java | 2 +- .../elearning/service/CourseServiceTest.java | 8 +- 4 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/backend/elearning/domain/course/CourseRepositoryCustomImpl.java diff --git a/src/main/java/com/backend/elearning/domain/course/CourseRepositoryCustomImpl.java b/src/main/java/com/backend/elearning/domain/course/CourseRepositoryCustomImpl.java new file mode 100644 index 00000000..ca32a4e1 --- /dev/null +++ b/src/main/java/com/backend/elearning/domain/course/CourseRepositoryCustomImpl.java @@ -0,0 +1,158 @@ +package com.backend.elearning.domain.course; + +import com.backend.elearning.domain.category.Category; +import com.backend.elearning.domain.common.PageableData; +import com.backend.elearning.domain.review.Review; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Repository +public class CourseRepositoryCustomImpl { + + + @PersistenceContext + private EntityManager entityManager; + + public Page findByMultiFilter( + String title, + Float ratingStar, + String[] level, + Boolean[] free, + String categoryName, + Integer topicId, + Pageable pageable) { + + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + // ----------- MAIN QUERY ------------- + CriteriaQuery cq = cb.createQuery(Course.class); + Root root = cq.from(Course.class); + root.fetch("category", JoinType.INNER).fetch("parent", JoinType.LEFT); + root.fetch("topic", JoinType.INNER); + root.fetch("user", JoinType.INNER); + cq.distinct(true); + + List predicates = new ArrayList<>(); + + // level IN (...) + if (level != null && level.length > 0) { + CriteriaBuilder.In levelIn = cb.in(root.get("level")); + for (String l : level) { + levelIn.value(l); + } + predicates.add(levelIn); + } + + // free IN (...) + if (free != null && free.length > 0) { + CriteriaBuilder.In freeIn = cb.in(root.get("free")); + for (Boolean f : free) { + freeIn.value(f); + } + predicates.add(freeIn); + } + + if (title != null && !title.isBlank()) { + predicates.add(cb.like(cb.lower(root.get("title")), "%" + title.toLowerCase() + "%")); + } + + // categoryName = cat.name or parent.name + if (categoryName != null) { + Join categoryJoin = root.join("category"); + Join parentJoin = categoryJoin.join("parent", JoinType.LEFT); + Predicate matchCat = cb.equal(categoryJoin.get("name"), categoryName); + Predicate matchParent = cb.equal(parentJoin.get("name"), categoryName); + predicates.add(cb.or(matchCat, matchParent)); + } + + // topicId + if (topicId != null) { + predicates.add(cb.equal(root.get("topic").get("id"), topicId)); + } + + // status = 'PUBLISHED' + predicates.add(cb.equal(root.get("status"), "PUBLISHED")); + + // Subquery for avg rating + if (ratingStar != null) { + Subquery subquery = cq.subquery(Double.class); + Root reviewRoot = subquery.from(Review.class); + subquery.select(cb.avg(reviewRoot.get("ratingStar"))); + subquery.where(cb.equal(reviewRoot.get("course").get("id"), root.get("id"))); + predicates.add(cb.greaterThanOrEqualTo(cb.coalesce(subquery, 0d), ratingStar.doubleValue())); + } + + cq.where(cb.and(predicates.toArray(new Predicate[0]))); + + // ----------- EXECUTE MAIN QUERY ------------- + TypedQuery query = entityManager.createQuery(cq); + query.setFirstResult((int) pageable.getOffset()); + query.setMaxResults(pageable.getPageSize()); + List courses = query.getResultList(); + + // ----------- COUNT QUERY ------------- + CriteriaQuery countQuery = cb.createQuery(Long.class); + Root countRoot = countQuery.from(Course.class); + countQuery.select(cb.countDistinct(countRoot)); + + List countPredicates = new ArrayList<>(); + + // Copy same predicates (repeat manually to avoid fetch joins) + if (level != null && level.length > 0) { + CriteriaBuilder.In levelIn = cb.in(countRoot.get("level")); + for (String l : level) { + levelIn.value(l); + } + countPredicates.add(levelIn); + } + + if (title != null && !title.isBlank()) { + countPredicates.add(cb.like(cb.lower(countRoot.get("title")), "%" + title.toLowerCase() + "%")); + } + + if (free != null && free.length > 0) { + CriteriaBuilder.In freeIn = cb.in(countRoot.get("free")); + for (Boolean f : free) { + freeIn.value(f); + } + countPredicates.add(freeIn); + } + + if (categoryName != null) { + Join categoryJoin = countRoot.join("category"); + Join parentJoin = categoryJoin.join("parent", JoinType.LEFT); + Predicate matchCat = cb.equal(categoryJoin.get("name"), categoryName); + Predicate matchParent = cb.equal(parentJoin.get("name"), categoryName); + countPredicates.add(cb.or(matchCat, matchParent)); + } + + if (topicId != null) { + countPredicates.add(cb.equal(countRoot.get("topic").get("id"), topicId)); + } + + countPredicates.add(cb.equal(countRoot.get("status"), "PUBLISHED")); + + if (ratingStar != null) { + Subquery subquery = countQuery.subquery(Double.class); + Root reviewRoot = subquery.from(Review.class); + subquery.select(cb.avg(reviewRoot.get("ratingStar"))); + subquery.where(cb.equal(reviewRoot.get("course").get("id"), countRoot.get("id"))); + countPredicates.add(cb.greaterThanOrEqualTo(cb.coalesce(subquery, 0d), ratingStar.doubleValue())); + } + + countQuery.where(cb.and(countPredicates.toArray(new Predicate[0]))); + Long total = entityManager.createQuery(countQuery).getSingleResult(); + + return new PageImpl<>(courses, pageable, total); + } +} diff --git a/src/main/java/com/backend/elearning/domain/course/CourseServiceImpl.java b/src/main/java/com/backend/elearning/domain/course/CourseServiceImpl.java index 902c32f5..4aa833e9 100644 --- a/src/main/java/com/backend/elearning/domain/course/CourseServiceImpl.java +++ b/src/main/java/com/backend/elearning/domain/course/CourseServiceImpl.java @@ -36,7 +36,6 @@ import org.springframework.data.domain.*; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.*; @@ -57,13 +56,16 @@ public class CourseServiceImpl implements CourseService{ private final LearningQuizRepository learningQuizRepository; private final LearningCourseRepository learningCourseRepository; private final OrderDetailRepository orderDetailRepository; + + private final CourseRepositoryCustomImpl courseRepositoryCustom; private final CartRepository cartRepository; private final UserService userService; private final UserRepository userRepository; private static final String LECTURE_TYPE = "lecture"; private static final String QUIZ_TYPE = "quiz"; private static final String sortBy = "updatedAt"; - public CourseServiceImpl(CourseRepository courseRepository, CategoryRepository categoryRepository, TopicRepository topicRepository, SectionService sectionService, QuizRepository quizRepository, LectureRepository lectureRepository, ReviewService reviewService, LearningLectureRepository learningLectureRepository, LearningQuizRepository learningQuizRepository, LearningCourseRepository learningCourseRepository, OrderDetailRepository orderDetailRepository, CartRepository cartRepository, UserService userService, UserRepository userRepository) { + + public CourseServiceImpl(CourseRepository courseRepository, CategoryRepository categoryRepository, TopicRepository topicRepository, SectionService sectionService, QuizRepository quizRepository, LectureRepository lectureRepository, ReviewService reviewService, LearningLectureRepository learningLectureRepository, LearningQuizRepository learningQuizRepository, LearningCourseRepository learningCourseRepository, OrderDetailRepository orderDetailRepository, CourseRepositoryCustomImpl courseRepositoryCustom, CartRepository cartRepository, UserService userService, UserRepository userRepository) { this.courseRepository = courseRepository; this.categoryRepository = categoryRepository; this.topicRepository = topicRepository; @@ -75,6 +77,7 @@ public CourseServiceImpl(CourseRepository courseRepository, CategoryRepository c this.learningQuizRepository = learningQuizRepository; this.learningCourseRepository = learningCourseRepository; this.orderDetailRepository = orderDetailRepository; + this.courseRepositoryCustom = courseRepositoryCustom; this.cartRepository = cartRepository; this.userService = userService; this.userRepository = userRepository; @@ -354,11 +357,8 @@ public PageableData getCoursesByMultiQuery(int pageNum, log.info("received pageNum: {}, pageSize: {}, title: {}, rating: {}, level: {}, free: {}, categoryName: {}, " + "topicId: {}", pageNum, pageSize, title, rating, level, free, categoryName, topicId); Pageable pageable = PageRequest.of(pageNum, pageSize); - Page coursePage = title != null ? courseRepository.findByMultiQueryWithKeyword(pageable, title, rating, level, free, categoryName, topicId) : - courseRepository.findByMultiQuery(pageable, rating, level, free, categoryName, topicId); + Page coursePage = courseRepositoryCustom.findByMultiFilter(title, rating, level, free, categoryName, topicId, pageable); List courses = coursePage.getContent(); -// List courses = title != null ? courseRepository.findByMultiQueryWithKeyword(title, rating, level, free, categoryName, topicId) : -// courseRepository.findByMultiQuery(rating, level, free, categoryName, topicId); List courseListGetVMS = courses.stream().map(course -> { course = courseRepository.findByIdWithPromotions(course).orElseThrow(() -> new NotFoundException(Constants.ERROR_CODE.COURSE_NOT_FOUND)); List reviews = course.getReviews(); diff --git a/src/main/java/com/backend/elearning/domain/student/StudentServiceImpl.java b/src/main/java/com/backend/elearning/domain/student/StudentServiceImpl.java index aaa9053e..b6eb847d 100644 --- a/src/main/java/com/backend/elearning/domain/student/StudentServiceImpl.java +++ b/src/main/java/com/backend/elearning/domain/student/StudentServiceImpl.java @@ -70,7 +70,7 @@ public UserVm updateProfileStudent(StudentPutVM studentPutVM) { if (studentPutVM.photo() != null && !studentPutVM.photo().isEmpty() && !studentPutVM.photo().isBlank()) { student.setPhoto(studentPutVM.photo()); } - if (studentPutVM.photo() != null && !studentPutVM.password().isEmpty() && !studentPutVM.password().isBlank() ) { + if (studentPutVM.password() != null && !studentPutVM.password().isEmpty() && !studentPutVM.password().isBlank()) { student.setPassword(passwordEncoder.encode(studentPutVM.password())); } // update diff --git a/src/test/java/com/backend/elearning/service/CourseServiceTest.java b/src/test/java/com/backend/elearning/service/CourseServiceTest.java index 249fefb8..da05d07f 100644 --- a/src/test/java/com/backend/elearning/service/CourseServiceTest.java +++ b/src/test/java/com/backend/elearning/service/CourseServiceTest.java @@ -1,6 +1,5 @@ package com.backend.elearning.service; -import com.backend.elearning.domain.auth.AuthenticationService; import com.backend.elearning.domain.cart.Cart; import com.backend.elearning.domain.cart.CartRepository; import com.backend.elearning.domain.category.Category; @@ -22,10 +21,8 @@ import com.backend.elearning.domain.user.User; import com.backend.elearning.domain.user.UserRepository; import com.backend.elearning.domain.user.UserService; -import com.backend.elearning.domain.user.UserVm; import com.backend.elearning.exception.BadRequestException; import com.backend.elearning.exception.DuplicateException; -import com.backend.elearning.exception.NotFoundException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -83,12 +80,15 @@ public class CourseServiceTest { @Mock private SecurityContext securityContext; + @Mock + private CourseRepositoryCustomImpl courseRepositoryCustom; + @Mock private Authentication authentication; @BeforeEach void beforeEach() { courseService = new CourseServiceImpl(courseRepository, categoryRepository, topicRepository, sectionService, - quizRepository, lectureRepository, reviewService, learningLectureRepository, learningQuizRepository, learningCourseRepository, orderDetailRepository, cartRepository + quizRepository, lectureRepository, reviewService, learningLectureRepository, learningQuizRepository, learningCourseRepository, orderDetailRepository, courseRepositoryCustom, cartRepository ,userService, userRepository); SecurityContextHolder.setContext(securityContext);