QueryDSL은 하이버네이트 쿼리 언어(HQL: Hibernate Query Language)의 쿼리를 타입에 안전하게 생성 및 관리해주는 프레임워크이다.
QueryDSL은 정적 타입을 이용하여 SQL과 같은 쿼리를 생성할 수 있게 해 준다.
⭐️ 자바 백엔드 기술은 Spring Boot와 Spring Data JPA를 함께 사용한다. 하지만, 복잡한 쿼리, 동적 쿼리를 구현하는 데 있어 한계가 있다.
QueryDSL이 등장하기 이전에는 Mybatis, JPQL, Criteria 등 문자열 형태로 쿼리문을 작성하여 컴파일 시에 오류를 발견하는 것이 불가능했다.
하지만, QueryDSL은 자바 코드로 SQL 문을 작성할 수 있어 컴파일 시에 오류를 발생하여 잘못된 쿼리가 실행되는 것을 방지할 수 있다.
QueryDSL을 사용하기 위해 먼저 엔티티 클래스에 대응하는 쿼리 타입(Q타입) 클래스를 생성해야 합니다. 쿼리 타입 클래스는 엔티티의 속성과 관련된 필드와 메서드를 가지며, 쿼리 작성에 사용됩니다. 쿼리 타입 클래스는 Annotation Processor를 사용하여 컴파일 시점에 생성됩니다.
생성된 쿼리 타입 클래스를 사용하여 타입 안전한 쿼리를 작성합니다. QueryDSL은 Fluent API를 제공하여 쿼리를 체인 형태로 작성할 수 있도록 합니다. 예를 들어, 테이블과 조인, 필터링, 정렬 등의 쿼리 조건을 메서드 체인으로 표현할 수 있습니다.
작성된 쿼리를 실행하기 위해 JPAQueryFactory를 생성하고, 쿼리 타입과 연결하여 쿼리를 실행합니다. JPAQueryFactory는 EntityManager와 연동하여 쿼리를 수행하고 결과를 반환합니다.
실행된 쿼리의 결과는 엔티티 객체 또는 DTO 객체로 반환됩니다. QueryDSL은 결과를 컬렉션 형태로 반환하며, fetch(), fetchOne(), fetchFirst() 등 다양한 반환 메서드를 제공합니다.
이러한 원리를 통해 QueryDSL은 자바 코드로 쉽게 SQL 쿼리를 작성하고 실행할 수 있으며, 타입 안정성과 가독성을 높일 수 있습니다. 또한, QueryDSL은 동적 쿼리 작성과 쿼리 조합을 지원하여 유연한 쿼리 생성이 가능하도록 합니다.
querydsl-apt: Querydsl 관련 코드 생성 기능 제공
querydsl-jpa: querydsl 라이브러리
Querydsl은 JPQL 빌더
JPQL: 문자(실행 시점 오류), Querydsl: 코드(컴파일 시점 오류) JPQL: 파라미터 바인딩 직접, Querydsl: 파라미터 바인딩 자동 처리
package study.querydsl;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import study.querydsl.entity.Member;
import study.querydsl.entity.QMember;
import study.querydsl.entity.Team;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
import static study.querydsl.entity.QMember.*;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
public void before() {
queryFactory = new JPAQueryFactory(em);
//...
}
@Test
public void startQuerydsl2() {
//member1을 찾아라.
QMember m = new QMember("m");
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
}
JPAQueryFactory를 필드로 제공하면 동시성 문제는 어떻게 될까? 동시성 문제는 JPAQueryFactory를 생성할 때 제공하는 EntityManager(em)에 달려있다. 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱 정하지 않아도 된다. 즉 @PersistenceContext 으로 주입 받은 EntityManager 를 proxy로 감싼 후 , 호출 할때마다 proxy를 통해 entityManager가 생성하여 Thread-Safe를 보장한다.
Q클래스 인스턴스를 사용하는 2가지 방법
QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
⭐️ 같은 테이블을 조인해야 하는 경우가 아니면 기본 인스턴스를 사용하자
import static study.querydsl.entity.QMember.*;
@Test
public void startQuerydsl3() {
//member1을 찾아라.
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
spring.jpa.properties.hibernate.use_sql_comments: true
주석처리되어 콘솔창에 출력된다.
@Test
public void search() {
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1")
.and(member.age.eq(10)))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
검색조건은 .and(),.or()를메서드체인으로연결할수있다.
참고: select , from 을 selectFrom 으로 합칠 수 있음
member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'
member.username.isNotNull() //이름이 is not null
member.age.in(10, 20) // age in (10,20)
member.age.notIn(10, 20) // age not in (10, 20)
member.age.between(10,30) //between 10, 30
member.age.goe(30) // age >= 30
member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30
member.age.lt(30) // age < 30
member.username.like("member%") //like 검색 member.username.contains("member") // like ‘%member%’ 검색 member.username.startsWith("member") //like ‘member%’ 검색 ...
@Test
public void searchAndParam() {
List<Member> result1 = queryFactory
.selectFrom(member)
.where(member.username.eq("member1"),
member.age.eq(10))
.fetch();
assertThat(result1.size()).isEqualTo(1);
}
//List
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
//단 건
Member findMember1 = queryFactory
.selectFrom(member)
.fetchOne();
//처음 한 건 조회
Member findMember2 = queryFactory
.selectFrom(member)
.fetchFirst();
//페이징에서 사용
QueryResults<Member> results = queryFactory
.selectFrom(member)
.fetchResults();
//count 쿼리로 변경
long count = queryFactory
.selectFrom(member)
.fetchCount();
참고
김영한 QueryDSL 강의 자료
[Spring] QueryDsl gradle 설정 (Spring boot 3.x , 2.x )