일반적으로 Spring Boot에서 데이터 베이스를 다루는 데 사용하는 JPA(Java Persistence API)는 복잡한 쿼리를 구현하는 데 한계가 있다.
이러한 복잡한 쿼리를 해결하기 위해 JPQL(Java Persistence Query Language)나 네이티브SQL을 사용하는데, 이 방식은 문자열 기반이므로 오타나 문법 오류가 발생했을 때 컴파일 시점이 아닌 런타임에서 오류를 발견할 수 있다.
QueryDSL은 이러한 SQL문을 코드로 작성하게 함으로써 컴파일 시점에 오류를 잡아낼 수 있게 해준다.
타입 안정성
쿼리를 작성할 때 타입 안정성을 보장하여 컴파일 타임에 오류를 검출할 수 있다.
자동 생성된 QClass
각 엔티티에 대해 QClass가 자동 생성되어, 쿼리 작성 시 이를 활용할 수 있다.
동적 쿼리
쿼리 작성 시 동적 조건을 추가하거나 변경할 때 편리하다.
성능
쿼리 DSL 사용 시 조인 등 복잡한 쿼리의 최적화가 가능하다.
컴파일 단계에서 엔티티를 기반으로 생성되며, JPA_APT(JPAAnnotationProcessTool)가 @Entity와 같은 특정 어노테이션을 찾고 해당 클래스를 분석해 생성한다.
엔티티 클래스의 메타 정보를 담고 있는 클래스로, QueryDSL은 이를 이용하여 Type-Safe를 보장하며 쿼리를 작성할 수 있게 된다.
엔티티 속성의 타입을 정확하게 표현하므로, 타입에 맞지 않는 연산이나 비교를 시도하면 컴파일러가 오류를 감지할 수 있다.
종합적으로 QClass를 사용하면 런타임에 발생할 수 있는 잠재적 오류를 컴파일 시점에 미리 잡아내어 코드의 안정성을 높혀준다.
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"
build/generated/sources/annotationProcessor/java/main
에 Q클래스가 생성된다.expert/tasks/other/compile.java
를 실행시켜 보는것을 권장한다.)import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JPAConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
JPAQueryFactory를 사용하기 위해 @Configuration
을 통해 설정 클래스를 추가해 줄 필요가 있다.
QueryDSL이 JPA를 통해 엔티티를 조회하기 때문에 EntityManager를 JPAQueryFactory 생성자에 주입해 반환해준다.
@Repository
구현체만을 생성해 사용 할 수 있지만, 기존 사용하던 JPARepository와 함께 사용하기 위해 인터페이스를 만들어 구현하는 방법을 사용했다.package org.example.expert.domain.todo.repository;
import org.example.expert.domain.todo.entity.Todo;
import java.util.Optional;
public interface TodoRepositoryQuery {
Optional<Todo> findByIdWithUser(Long todoId);
}
public interface TodoRepository extends JpaRepository<Todo, Long> ,TodoRepositoryQuery{
}
package org.example.expert.domain.todo.repository;
import static org.example.expert.domain.todo.entity.QTodo.todo;
import static org.example.expert.domain.user.entity.QUser.user;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.example.expert.domain.todo.entity.Todo;
import java.util.Optional;
@RequiredArgsConstructor
public class TodoRepositoryQueryImpl implements TodoRepositoryQuery {
private final JPAQueryFactory jpaQueryFactory;
@Override
public Optional<Todo> findByIdWithUser(Long todoId) {
return Optional.ofNullable(
jpaQueryFactory
.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(todo.id.eq(todoId))
.fetchOne());
}
}
@Query("SELECT t FROM Todo t " +
"LEFT JOIN FETCH t.user " +
"WHERE t.id = :todoId")
Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);
QueryDSL문에서 마지막에 붙는 .fetchOne()
은 조회한 엔티티를 반환하기 위한 옵션이다.
fetchOne : 하나의 결과를 가져오고 싶을 때 사용
fetch : 리스트 형태와 같이 여러 개의 결과를 가져오고 싶을 때 사용
fetchFirst : 여러개의 결과 중 첫 번째 결과만 가져오고 싶을 때 사용
(limit(1).fetchOne()과 같은 결과)
public List<User> findUsersByNameAndEmail(String name, String email, LocalDate startDate, LocalDate endDate)
QUser user = QUser.user;
BooleanBuilder builder = new BooleanBuilder();
// WHERE :name IS NOT NULL AND user.name = :name
if (name != null && !name.isEmpty()) {
builder.and(user.name.eq(name));
}
// WHERE :email IS NOT NULL AND user.email LIKE CONCAT ('%', :email, '%')
if (email != null && !email.isEmpty()) {
// user.name.like("'%"+name+"%'")과 동일한 기능
builder.and(user.email.contains(email));
}
// startDate와 endDate를 LocalDateTime으로 변환 후
// WHERE :startDate IS NOT NULL AND :endDate IS NOT NULL AND
// user.createdAt >= :startDate AND uesr.createdAt <= :endDate
if(startDate != null && endDate != null) {
builder.and(user.createdAt.between(startDate.atStartOfDay(), endDate.atTime(LocalTime.MAX)));
}
return jpaQueryFactory
.selectFrom(user)
.where(builder)
.fetch();
}
BooleanBuilder와 같은 동적 쿼리이다.
조건문 쿼리들을 분리해 메서드 단위로 관리하기 때문에 조건문이 많아질 때 사용하기 적합하다.
public List<User> findUsersByName(String name)
QUser user = QUser.user;
...
return jpaQueryFactory
.selectFrom(user)
.where(usernameEq(name))
.fetch();
}
private BooleanExpression usernameEq(String username) {
if (username == null || username.isEmpty()) {
return null;
}
// user.name.eq(username)와 동일한 BooleanExpression 객체를 반환
return user.name.eq(username);
}
BooleanExpression or = usernameEq().or(useremailEq())
BooleanExpression and = usernameEq().and(useremailEq())
List<User> users = queryFactory
.selectFrom(user)
.where(...)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// 전체 데이터 개수 (count 쿼리)
JPAQuery<Long> totalQuery = queryFactory
.select(user.count())
.from(user)
.where(...);
// Page 객체 생성 및 반환
return PageableExecutionUtils.getPage(users, pageable, totalQuery::fetchOne);
.offset()
과 .limit()
으로 List를 만든 뒤 Page를 생성해 반환한다.
PageableExecutionUtils은 Spring Data JPA에서 Page 객체를 최적화하여 반환하는 유틸리티 클래스이
다.
PageableExecutionUtils 대신 new PageImpl()
을 사용할 수도 있는데,
new PageImpl()
은 데이터를 조회하는 쿼리와 전체 개수를 조회하는 쿼리를 모두 호출해야 하므로 PageableExecutionUtils를 사용해 불필요한 카운트 쿼리를 줄일 수 있다.
Projections
는 엔티티에서 필요한 필드만 가져와서 DTO로 반환해 준다.Projections.constructor()
: 생성자 기반 매핑List<TodoSearchResponse> list = jpaQueryFactory
.select(Projections.constructor(
TodoSearchResponse.class,
todo.title,
todo.managers.size(),
todo.comments.size()
))
DTO에도 생성자에 @QueryProjection
를 붙이면 QClass가 생성된다.
장점
단점
https://turtle-codingstudy.tistory.com/54
https://3uomlkh.tistory.com/383