Spring

[Spring] Spring Boot에서 JPA QueryDSL 사용 | QueryDSL 사용법 | QueryDSL 의존성 추가

kimslab01 2024. 10. 3. 22:20

 

 

 

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로 작성된 findByIdWithUserQueryDSL로 변경합니다.
  • 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 클래스가 만들어집니다!!

 

 

https://g-db.tistory.com/entry/Spring-Boot-Spring-Boot%EC%97%90%EC%84%9C-JPA-QueryDSL-%EC%A0%81%EC%9A%A9-%EB%B0%A9%EB%B2%95

 

[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 문제가 발생하지 않도록 조건에 맞게 해결할 수 있습니다!