QueryDsl 소개
QueryDSL이란?
QueryDSL은 SQL과 JPQL (Java Persistence Query Language)을 Java 코드로 작성할 수 있게 도와주는 동적 쿼리 빌더이다.
이를 통해 SQL 쿼리를 문자열로 작성하는 대신, Java 객체를 사용하여 쿼리를 작성할 수 있다. QueryDSL은 JPA, Hibernate, MongoDB 등 다양한 데이터베이스와 함께 사용할 수 있으며, 타입 안전성을 보장한다.
QueryDSL의 장점
- 타입 안전성: QueryDSL은 Java 문법을 사용하여 쿼리를 작성하기 때문에 컴파일 시점에 문법 오류를 검출할 수 있다. 이는 런타임 에러를 줄이고, 안전한 쿼리 작성을 가능하게 한다.
- 동적 쿼리 작성: 다양한 조건을 동적으로 추가하거나 제거할 수 있어 복잡한 쿼리를 간결하게 표현할 수 있다. 이를 통해 코드의 가독성과 유지보수성이 향상된다.
- IDE 지원: QueryDSL을 사용하면 IDE의 자동 완성 기능을 활용할 수 있어 쿼리 작성이 더 쉬워진다. 또한, 코드 내에서 쿼리를 작성하기 때문에 코드의 일관성을 유지할 수 있다.
- 재사용성: 쿼리 조건을 메서드로 분리하여 재사용할 수 있다. 이는 코드의 중복을 줄이고, 유지보수를 용이하게 한다.
QueryDSL 기본 사용법
- Q타입 클래스: 엔티티 클래스에 대응하는 Q타입 클래스를 사용하여 쿼리를 작성한다.
- JPAQueryFactory: 쿼리를 생성하고 실행하는 핵심 클래스이다.
- BooleanExpression: 조건을 표현하는 클래스이다.
Q타입 클래스 생성
Q타입 클래스는 엔티티 클래스에 대응하는 클래스로, 쿼리에서 엔티티 필드를 타입 안전하게 사용할 수 있도록 한다. Q타입 클래스는 컴파일 시 자동으로 생성된다. 예를 들어, User 엔티티가 있다면, QUser 클래스가 생성된다
쿼리 작성 예제
1.기본 조회
QUser user = QUser.user;
List<User> users = queryFactory
.selectFrom(user)
.where(user.age.gt(18)) // 나이가 18세 이상인 사용자
.fetch();
2.복합 조건 사용
List<User> users = queryFactory
.selectFrom(user)
.where(
user.name.eq("John"), // 이름이 John인 사용자
user.age.between(18, 30), // 나이가 18세에서 30세 사이인 사용자
user.isActive.isTrue() // 활성화된 사용자
)
.fetch();
3. 정렬
List<User> users = queryFactory
.selectFrom(user)
.where(user.age.gt(18))
.orderBy(user.name.asc()) // 이름을 오름차순으로 정렬
.fetch();
4. 페이징
List<User> users = queryFactory
.selectFrom(user)
.where(user.age.gt(18))
.orderBy(user.name.asc())
.offset(10) // 10번째 결과부터 시작
.limit(5) // 최대 5개 결과 반환
.fetch();
5. 조인
QOrder order = QOrder.order;
List<Tuple> result = queryFactory
.select(user, order)
.from(user)
.leftJoin(user.orders, order) // User와 Order를 left join
.where(order.price.gt(100)) // 주문 가격이 100 이상인 경우
.fetch();
왜 굳이 QClass를 사용할까?
QueryDSL의 장점을 활용하기 위해서는 QClass가 꼭 필요한데, querydsl 구상 당시에 qclass를 사용하지 않고도 entity만으로 querydsl을 사용하는 법을 구상해봤을거같은데 어떤 문제점이 있었을까?
사실 엔티티 클래스만으로 쿼리를 작성하는 방법이 있다. JPQL이다.
JPQL을 사용해보면 QClass의 장점이 명확해지는데 JPQL을 사용하다 보면 불편한점이 필드 이름을 문자열로 직접 입력해야 하는 경우가 많다는 것이다.
// 엔티티 클래스
@Entity
public class User {
@Id
private Long id;
private String name;
private Integer age;
private Boolean isActive;
// getters and setters
}
// 쿼리 작성
public List<User> findActiveUsers(EntityManager em) {
String jpql = "SELECT u FROM User u WHERE u.isActive = true";
return em.createQuery(jpql, User.class).getResultList();
}
이처럼 JPQL은 쿼리의 오타나 잘못된 필드 이름으로 인해 런타임 오류를 발생시킬 수 있다. Q타입 클래스는 컴파일 시점에 타입 검사를 통해 이러한 오류를 방지할 수 있다.
유지보수성
만약 여러곳에서 사용되는 repository 메소드를 만든다고 가정해보자, 예를 들어 유저를 찾는데 다양한 조건의 유저를 찾기 위한 메소드가 필요하다면?
JPQL
public List<User> findUsers(String name, Integer age, Boolean isActive) {
String jpql = "SELECT u FROM User u WHERE 1=1";
if (name != null) {
jpql += " AND u.name = :name";
}
if (age != null) {
jpql += " AND u.age = :age";
}
if (isActive != null) {
jpql += " AND u.isActive = :isActive";
}
TypedQuery<User> query = em.createQuery(jpql, User.class);
if (name != null) {
query.setParameter("name", name);
}
if (age != null) {
query.setParameter("age", age);
}
if (isActive != null) {
query.setParameter("isActive", isActive);
}
return query.getResultList();
}
Querydsl
public List<User> findUsers(String name, Integer age, Boolean isActive) {
QUser user = QUser.user;
BooleanBuilder builder = new BooleanBuilder();
if (name != null) {
builder.and(user.name.eq(name));
}
if (age != null) {
builder.and(user.age.eq(age));
}
if (isActive != null) {
builder.and(user.isActive.eq(isActive));
}
return queryFactory
.selectFrom(user)
.where(builder)
.fetch();
}
위의 예시는 주어진 필드에 따라서 다양한 유저를 select 할 수 있는 repository 메소드의 예시인데, 단지 필드가 2개일 뿐인데도 JPQL을 사용했을때는 다양한 조건을 설정해주어야하고 이마저도 문자열로 작성하여 생산성도 굉장히 떨어진다.
만약 JPQL을 사용하는 개발자가 해당 메소드에 필터링 할 수 있는 필드를 5개 더 추가해 달라는 요청을 받으면 굉장히 머리가 아플것이다..
querydsl은 BooleanBuilder라는 동적으로 조건을 생성할 수 있는 클래스를 사용하여 해당 문제를 간단하게 해결할 수 있다.