JPQL vs Querydsl
테스트 기본 코드
package study.querydsl.entity;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@Autowired
EntityManager em;
@BeforeEach
public void before() {
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamA);
Member member4 = new Member("member4", 40, teamA);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
}
member1을 조회 해보자
//JPQL
@Test
public void startJPQL() {
Member findMember = em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
//Querydsl
@Test
public void startQuerydsl() {
//em이용해 JPAQueryFactory 생성 → 쿼리 문법에서 실수 방지
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
//QueryDSL이 자동 생성한 QMember 클래스 사용 → 엔티티 필드명에서 실수 방지
QMember m = new QMember("m"); //"m" = member as m (alias 이름)
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1"))
.fetchOne(); // 결과 하나만 조회 (없으면 null, 두 개 이상이면 예외)
assertThat(findMember.getUsername()).isEqualTo("member1");
}
JPQL : 문자 (실행 시점 오류) vs Querydsl : 코드 (컴파일 시점 오류)
JPQL : 파라미터 바인딩 직접 vs Querydsl : 파라미터 바인딩 자동 처리
| QMember | Member 엔티티용으로 자동 생성된 클래스 |
| QMember.member | Member 테이블에 대한 쿼리를 도와주는 정적 인스턴스 |
JPAQueryFactory 필드
JPAQueryFactory를 필드로 제공하면
동시성 문제는 JPAQueryFactory를 생성할 때 제공하는EntityManager(em)에 달려있게됨.
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
public void before() {
queryFactory = new JPAQueryFactory(em);
//…
}
@Test
public void startQuerydsl2() {
QMember m = new QMember("m");
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
}
스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도,
트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 됨.
기본 Q-Type 활용
Q클래스 인스턴스를 사용
QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
기본 인스턴스 사용이 더 간편
static import와 함께 사용 (권장)
import static study.querydsl.entity.QMember.member;
...
@Test
public void startQuerydsl2() {
Member findMember = queryFactory
.select(member) //QMember.member 치고 단축키
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
실행되는 JPQL보기
-application.yml
# JPA 설정
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
#show_sql: true # 쿼리 출력 여부(sout)
format_sql: true # 보기 좋게 출력
use_sql_comments: true # JPQL → SQL 변환 시 주석으로 JPQL 내용 표시
결과
/* select
member1
from
Member member1
where
member1.username = ?1 */ select
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
member m1_0
where
m1_0.username=?
/* 첫번째 JPQL, 두번째 Querydsl
검색 조건 쿼리
기본 검색 쿼리
@Test
public void search() {
Member findMember = queryFactory
.selectFrom(member) //select, from 합칠 수 있음
.where(member.username.eq("member1")
.and(member.age.eq(10)))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
검색조건 .and(), .or() 사용해 이어 붙일 수 있음
모든 검색 조건 (JPQL 제공)
member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'
member.username.isNotNull()
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%’ 검색 ...
g = greater, l = less
t = than, oe = or equal
AND 조건을 파라미터로 처리
@Test
public void searchAndParam() {
List<Member> result1 = queryFactory
.selectFrom(member)
.where(
member.username.eq("member1"),
member.age.eq(10)) //(,) 추가시 AND조건 자동 적용
.fetch();
assertThat(result1.size()).isEqualTo(1);
}
.where(조건1, 조건2, 조건3) 나열하는 경우 null이면 무시됨
member.username.eq(null) → 내부에서 무시 (조건 안 붙음)
➡︎ 동적 쿼리 작성 시 매우 유용
결과 조회
- fetch(): 리스트 조회, 데이터 없으면 빈 리스트 반환
- fetchOne(): 단건 조회 ( 결과 없으면 null, 둘이상 예외)
- fetchFirst(): limit(1).fetchOne()와 같음
- fetchResults(): 데이터 + 카운트 쿼리 실행 (비권장)
- fetchCount(): count 쿼리 변경 . select count(-)
//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();
페이징에서 사용
fetchResults()는 QueryResults<T>를 반환
@Test
public void resultFetch() {
QueryResults<Member> results = queryFactory
.selectFrom(member)
.offset(0)
.limit(10)
.fetchResults();
long total = results.getTotal(); // 전체 개수(COUNT 쿼리 결과)
List<Member> content = results.getResults(); // 현재 페이지 데이터
fetchResults() - QueryDSL 5+에선 비권장
나눠서 하기
필요할때만 카운트 쿼리 실행 가능
// 1. 컨텐츠 쿼리
List<Member> content = queryFactory
.selectFrom(member)
.offset(0)
.limit(10)
.fetch();
// 2. 카운트 쿼리
Long total = queryFactory
.select(member.count())
.from(member)
.fetchOne();
정렬
1. 회원 나이 내림차순
2. 회원 이름 오름차순, 이름 없으면 마지막에 출력(nulls last)
@Test
public void sort() {
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
}
- desc(), asc() : 일반 정렬
- desc() : 내림차순
- asc() : 오름차순
- nullsLast(), nullsFirst() : null 데이터 순서 부여
페이징
조회 건수 제한
@Test
public void paging1() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1)
.limit(2)
.fetch();
assertThat(result.size()).isEqualTo(2);
}
}
전체 조회 수가 필요한 경우
@Test
public void paging2() {
QueryResults<Member> queryResults = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1)
.limit(2)
.fetchResults();
assertThat(queryResults.getTotal()).isEqualTo(4);
assertThat(queryResults.getLimit()).isEqualTo(2);
assertThat(queryResults.getOffset()).isEqualTo(1);
assertThat(queryResults.getResults().size()).isEqualTo(2);
}
집합
JPQL이 제공하는 모든 집합 함수를 제공
@Test
public void aggregation() {
List<Tuple> result = queryFactory
.select(
member.count(),
member.age.sum(),
member.age.avg(),
member.age.max(),
member.age.min()
)
.from(member)
.fetch();
Tuple tuple = result.get(0);
assertThat(tuple.get(member.count())).isEqualTo(4);
assertThat(tuple.get(member.age.sum())).isEqualTo(100);
assertThat(tuple.get(member.age.avg())).isEqualTo(25);
assertThat(tuple.get(member.age.max())).isEqualTo(40);
assertThat(tuple.get(member.age.min())).isEqualTo(10);
}
GroupBy 사용
@Test
public void group() throws Exception {
List<Tuple> result = queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.fetch();
Tuple teamA = result.get(0);
Tuple teamB = result.get(1);
assertThat(teamA.get(team.name)).isEqualTo("teamA");
assertThat(teamA.get(member.age.avg())).isEqualTo(15);
assertThat(teamB.get(team.name)).isEqualTo("teamB");
assertThat(teamB.get(member.age.avg())).isEqualTo(35);
}
.groupBy(item.price)
.having(item.price.gt(1000))