1. 바꾸어야 할 부분
public Page<TodoResponse> getTodos(int page, int size, String weather, LocalDateTime startDate, LocalDateTime endDate) {
Pageable pageable = PageRequest.of(page - 1, size);
Page<Todo> todos = todoRepository.findTodosByConditions(weather, startDate, endDate, pageable);
return todos.map(todo -> new TodoResponse(
todo.getId(),
todo.getTitle(),
todo.getContents(),
todo.getWeather(),
new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()),
todo.getCreatedAt(),
todo.getModifiedAt()
));
}
- JPQL로 작성된 findByIdWithUser를 QueryDSL로 변경합니다.
- N+1 문제가 발생하지 않도록 유의해 주세요!
2. 문제가 되는 부분 찾기
package org.example.expert.domain.todo.repository;
import org.example.expert.domain.todo.entity.Todo;
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.repository.query.Param;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Optional;
public interface TodoRepository extends JpaRepository<Todo, Long>, TodoRepositoryCustom {
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);
@Query("SELECT t FROM Todo t " +
"LEFT JOIN t.user " +
"WHERE t.id = :todoId")
Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);
@Query("SELECT t FROM Todo t " +
"LEFT JOIN t.user " +
"WHERE (:weather IS NULL OR t.weather = :weather) " +
"AND (:startDate IS NULL OR t.modifiedAt >= :startDate) " +
"AND (:endDate IS NULL OR t.modifiedAt <= :endDate) " +
"ORDER BY t.modifiedAt DESC")
Page<Todo> findTodosByConditions(
@Param("weather") String weather,
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate,
Pageable pageable
);
}
getTodos에서 findTodosByConditions 부분에서
JPQLQueryDSL을 사용하여 fetch join을 사용하여 관련된 엔티티(User)를 한 번의 쿼리로 조회하여 N+1 문제를 해결할 수 있습니다.
3. QueryDSL 의존성 추가
dependencies {
// QueryDSL
implementation 'com.querydsl:querydsl-apt:5.0.0'
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
implementation 'com.querydsl:querydsl-core:5.0.0'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
dependencies에 QueryDSL의 의존성을 입력하고
// Querydsl 빌드 옵션 설정
def generated = 'src/main/generated'
// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(generated))
}
// java source set 에 querydsl QClass 위치 추가
sourceSets {
main.java.srcDirs += [ generated ]
}
// gradle clean 시에 QClass 디렉토리 삭제
clean {
delete file(generated)
}
밑에는 Q 클래스 생성을 위한 코드를 입력해주세요.
다 입력하고 나면
이 사진처럼 되어야 합니다.
그리고
코끼리 모양을 눌려주고 Tasks 밑에 build에서 build를 눌러서 실행해주세요.
실행하고 나면
설정한 디렉토리를 따라서
이렇게 엔티티들의 Q 클래스가 만들어집니다!!
[Spring Boot] Spring Boot에서 JPA QueryDSL 적용 방법
개요 QueryDSL을 통해서 JPQL을 동적으로 구성할 수 있는 법을 공부하고 Spring Boot에 적용하는 방법을 찾아보고 적용시킨 방법을 기록한다. QueryDSL은 JPA에서 공식적으로 제공하는 JPQL 빌더가 아니기
g-db.tistory.com
이 블로그를 참고했습니다!
4. CustomImpl 클래스 만들기
Domain/Todo/Repository 밑에
TodoRepositoryCustom, TodoRepositoryImpl 두 클래스들을 생성해줍니다.
package org.example.expert.domain.todo.repository;
import org.example.expert.domain.todo.entity.Todo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.time.LocalDateTime;
public interface TodoRepositoryCustom {
Page<Todo> findTodosByConditions(String weather, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable);
}
package org.example.expert.domain.todo.repository;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.example.expert.domain.todo.entity.QTodo;
import org.example.expert.domain.todo.entity.Todo;
import org.example.expert.domain.user.entity.QUser;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import java.time.LocalDateTime;
import java.util.List;
@RequiredArgsConstructor
public class TodoRepositoryImpl implements TodoRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public Page<Todo> findTodosByConditions(String weather, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable) {
QTodo todo = QTodo.todo;
QUser user = QUser.user;
JPAQuery<Todo> query = queryFactory
.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(
weather != null ? todo.weather.eq(weather) : null,
startDate != null ? todo.modifiedAt.goe(startDate) : null,
endDate != null ? todo.modifiedAt.loe(endDate) : null
)
.orderBy(todo.modifiedAt.desc());
long total = query.fetchCount();
List<Todo> todos = query
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
return new PageImpl<>(todos, pageable, total);
}
}
5. TodoRepository 수정
기존의 TodoRepository에 있던 findTodosByContidions를 삭제해줍니다.
그리고 TodoRepositoryCustom을 extends 해줍니다.
package org.example.expert.domain.todo.repository;
import org.example.expert.domain.todo.entity.Todo;
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.repository.query.Param;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Optional;
public interface TodoRepository extends JpaRepository<Todo, Long>, TodoRepositoryCustom {
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);
@Query("SELECT t FROM Todo t " +
"LEFT JOIN t.user " +
"WHERE t.id = :todoId")
Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);
}
6. QueryDSL 빈 주입
domain/config에 QueryDSL를 사용할 수 있도록 빈을 주입해줍니다.
package org.example.expert.config;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QuerydslConfig {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
7. 기존에 있던 TodoService의 getTodos는 그대로 사용
이러면 JPQL로 된 쿼리를 QueryDSL을 사용하여 N+1 문제가 발생하지 않도록 조건에 맞게 해결할 수 있습니다!
'Spring' 카테고리의 다른 글
[Spring Boot] WebSocket 사용법 | 웹소켓 사용법 | 웹소켓 실시간 알림 | 웹소켓 프론트 | 웹소켓 테스트하는 법 | 웹소켓 html (0) | 2024.10.29 |
---|---|
[Spring] Spring Security 테스트 코드 401 에러 | Spring Security 테스트 코드 401 에러 해결법 (0) | 2024.10.06 |
[Spring] 스프링 카카오 소셜 로그인 구현 | 깃 카카오 소셜 로그인 코드 (5) | 2024.09.25 |
[Spring] 컨테이너 및 도커 개념정리 (0) | 2024.09.19 |
[Spring] 소셜로그인 연동 | 카카오 로그인 | 카카오 사용자 정보 가져오기 | 카카오 사용자 정보로 회원가입 구현 (0) | 2024.09.18 |