[Querydsl] 3. 기본 문법
📌 시작 - JPQL vs Querydsl
테스트 기본 코드
package study.querydsl;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
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 java.util.List;
import static org.assertj.core.api.Assertions.*;
import static study.querydsl.entity.QMember.member;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
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, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
}
⚡ Querydsl vs JPQL
@Test
public void startJPQL() {
//member1을 찾아라.
String qlString =
"select m from Member m " +
"where m.username = :username";
Member findMember = em.createQuery(qlString, Member.class)
.setParameter("username", "member1")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
@Test
public void startQuerydsl() {
//member1을 찾아라.
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember m = new QMember("m");
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1"))//파라미터 바인딩 처리 .fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
EntityManager
로JPAQueryFactory
생성- Querydsl은 JPQL 빌더
- JPQL: 문자(실행 시점 오류), Querydsl: 코드(컴파일 시점 오류)
- JPQL: 파라미터 바인딩 직접, Querydsl: 파라미터 바인딩 자동 처리
⚡ JPAQueryFactory를 필드로
package study.querydsl;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
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 java.util.List;
import static org.assertj.core.api.Assertions.*;
import static study.querydsl.entity.QMember.member;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
public void before() {
queryFactory = new JPAQueryFactory(em);
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, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
@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에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다.
📌 기본 Q-Type 활용
⚡ Q클래스 인스턴스를 사용하는 2가지 방법
QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
⚡ 기본 인스턴스를 static import와 함께 사용
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");
}
cf) 같은 테이블을 조인해야 하는 경우가 아니면 기본 인스턴스를 사용하자
➡ 다음 설정을 추가하면 실행되는 JPQL을 볼 수 있다.
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()
를 메서드 체인으로 연결할 수 있다.
cf) select , from 을 selectFrom 으로 합칠 수 있음
⚡ JPQL이 제공하는 모든 검색 조건 제공
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%’ 검색 ...
⚡ AND 조건을 파라미터로 처리
@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);
}
where()
에 파라미터로 검색조건을 추가하면AND
조건이 추가됨- 이경우
null
값은 무시 ➡ 메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있음
📌 결과 조회
fetch()
: 리스트 조회, 데이터 없으면 빈 리스트 반환fetchOne()
: 단 건 조회- 결과가 없으면 :
null
- 결과가 둘 이상이면 :
com.querydsl.core.NonUniqueResultException
- 결과가 없으면 :
fetchFirst()
:limit(1).fetchOne()
fetchResults()
: 페이징 정보 포함, total count 쿼리 추가 실행fetchCount()
: count 쿼리로 변경해서 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();
📌 정렬
/**
*회원 정렬 순서
* 1. 회원 나이 내림차순(desc)
* 2. 회원 이름 올림차순(asc)
* 단 2에서 회원 이름이 없으면 마지막에 출력(nulls last) */
@Test
public void sort() {
em.persist(new Member(null, 100));
em.persist(new Member("member5", 100));
em.persist(new Member("member6", 100));
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
Member member5 = result.get(0);
Member member6 = result.get(1);
Member memberNull = result.get(2);
assertThat(member5.getUsername()).isEqualTo("member5");
assertThat(member6.getUsername()).isEqualTo("member6");
assertThat(memberNull.getUsername()).isNull();
}
desc()
,asc()
: 일반 정렬nullsLast()
,nullsFirst()
: null 데이터 순서 부여
📌 페이징
⚡ 조건 건수 제한
@Test
public void paging1() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) //0부터 시작(zero index)
.limit(2) //최대 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);
}
주의❗) count 쿼리가 실행되니 성능상 주의!
cf)
실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, count 쿼리 는 조인이 필요 없는 경우도 있다. 그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인을 해버리기 때문에 성능이 안나올 수 있다.
count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, count 전용 쿼리를 별도로 작성해야 한다.
📌 집합
⚡ 집합 함수
/**
* JPQL
* select
* COUNT(m), //회원수
* SUM(m.age), //나이 합
* AVG(m.age), //평균 나이
* MAX(m.age), //최대 나이
* MIN(m.age) //최소 나이 * from Member m
*/
@Test
public void aggregation() throws Exception {
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);
}
- JPQL이 제공하는 모든 집합 함수를 제공한다.
- tuple은 프로젝션과 결과 반환에서 설명한다.
⚡ 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
, 그룹화된 결과를 제한하려면having
groupBy(), having() 예시
...
.groupBy(item.price)
.having(item.price.gt(1000))
...
📌 조인 - 기본 조인
⚡ 기본 조인
- 조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.
join(조인 대상, 별칭으로 사용할 Q타입)
/**
*팀A에 소속된 모든 회원 */
@Test
public void join() throws Exception {
QMember member = QMember.member;
QTeam team = QTeam.team;
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("member1", "member2");
}
join()
,innerJoin()
: 내부 조인(inner join)- leftJoin() : left 외부 조인(left outer join)
rightJoin()
: rigth 외부 조인(rigth outer join)- JPQL의
on
과 성능 최적화를 위한fetch
조인 제공 다음 on 절에서 설명
⚡ 세타 조인
- 연관관계가 없는 필드로 조인
/**
* 세타 조인(연관관계가 없는 필드로 조인)
* 회원의 이름이 팀 이름과 같은 회원 조회
*/
@Test
public void theta_join() throws Exception {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
List<Member> result = queryFactory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("teamA", "teamB");
}
- from 절에 여러 엔티티를 선택해서 세타 조인
- 외부조인불가능
- 다음에 설명 할 조인 on 을 사용하면 외부 조인 가능
📌 조인 - on 절
- ON절을 활용한 조인(JPA 2.1부터 지원)
- 조인 대상 필터링
- 연관관계 없는 엔티티 외부 조인
⚡ 1. 조인 대상 필터링
예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
@Test
public void join_on_filtering() throws Exception{
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(member.team, team).on(team.name.eq("teamA"))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = "+ tuple);
}
}
결과
t=[Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
t=[Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
t=[Member(id=5, username=member3, age=30), null]
t=[Member(id=6, username=member4, age=40), null]
cf)
on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where 절 에서 필터링 하는 것과 기능이 동일하다.
➡ on 절을 활용한 조인 대상 필터링을 사용할 때, 내부조인이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자.
⚡ 2. 연관관계 없는 엔티티 외부 조인
예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
/**
* 2. 연관관계 없는 엔티티 외부 조인
* 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
* JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
* SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name
*/
@Test
public void join_on_no_relation() throws Exception{
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(team).on(member.username.eq(team.name))
.fetch();
for (Tuple tuple : result) {
System.out.println("t=" + tuple);
}
}
결과
t=[Member(id=3, username=member1, age=10), null]
t=[Member(id=4, username=member2, age=20), null]
t=[Member(id=5, username=member3, age=30), null]
t=[Member(id=6, username=member4, age=40), null]
t=[Member(id=7, username=teamA, age=0), Team(id=1, name=teamA)]
t=[Member(id=8, username=teamB, age=0), Team(id=2, name=teamB)]
📌 조인 - 페치 조인
- 페치 조인은 SQL에서 제공하는 기능은 아니다.
- SQL조인을 활용해서 연관된 엔티티를 SQL 한번에 조회하는 기능이다.
- 주로 성능 최적화에 사용하는 방법이다.
⚡ 페치 조인 미적용
- 지연로딩으로 Member, Team SQL 쿼리 각각 실행
@PersistenceUnit
EntityManagerFactory emf;
@Test
public void fetchJoinNo() throws Exception {
em.flush();
em.clear();
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 미적용").isFalse();
}
⚡ 페치 조인 적용
- 즉시로딩으로 Member, Team SQL 쿼리 조인으로 한번에 조회 사용 방법
@Test
public void fetchJoinUse() throws Exception {
em.flush();
em.clear();
Member findMember = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 적용").isTrue();
}
join(), leftJoin()
등 조인 기능 뒤에fetchJoin()
이라고 추가하면 된다.
📌 서브 쿼리
com.querydsl.jpa.JPAExpressions
사용
⚡ 서브 쿼리 eq 사용
/**
* 나이가 가장 많은 회원 조회
*/
@Test
public void subQuery() throws Exception{
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub)
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(40);
}
⚡ 서브 쿼리 geo 사용
/**
* 서브 쿼리 goe 사용
*/
@Test
public void subQueryGoe() throws Exception{
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(30,40);
}
⚡ 서브 쿼리 여러 건 처리 in 사용
/**
* 서브쿼리 여러 건 처리, in 사용
*/
@Test
public void subQueryIn() throws Exception{
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.in(
JPAExpressions
.select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(20,30,40);
}
⚡ select 절에 subquery
@Test
public void selectSubQuery(){
QMember memberSub = new QMember("memberSub");
List<Tuple> fetch = queryFactory
.select(member.username,
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
).from(member)
.fetch();
for (Tuple tuple : fetch) {
System.out.println("username = "+ tuple.get(member.username));
System.out.println("age = "+ tuple.get(JPAExpressions.select(memberSub.age.avg())
.from(memberSub)));
}
}
⚡ static import 활용
import static com.querydsl.jpa.JPAExpressions.select;
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
select(memberSub.age.max())
.from(memberSub)
))
.fetch();
⚡ from 절의 서브쿼리 한계
- JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl도 지원하지 않는다.
- 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다.
- Querydsl도 하이버네이트 구현체를 사용 하면 select 절의 서브쿼리를 지원한다.
⚡ from 절의 서브쿼리 해결방안
- 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
- 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
- nativeSQL을 사용한다.
📌 Case 문
⚡select, 조건절(where), order by에서 사용 가능
✔ 단순한 조건
List<String> result = queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
✔ 복잡한 조건
List<String> result = queryFactory
.select(new CaseBuilder()
.when(member.age.between(0, 20)).then("0~20살")
.when(member.age.between(21, 30)).then("21~30살")
.otherwise("기타"))
.from(member)
.fetch();
✔ orderBy에서 Case 문 함께 사용하기 예제
예를 들어서 다음과 같은 임의의 순서로 회원을 출력하고 싶다면❓
1. 0~30살이아닌회원을가장먼저출력
2. 0~20살회원출력
3. 21~30살회원출력
NumberExpression<Integer> rankPath = new CaseBuilder()
.when(member.age.between(0, 20)).then(2)
.when(member.age.between(21, 30)).then(1)
.otherwise(3);
List<Tuple> result = queryFactory
.select(member.username, member.age, rankPath)
.from(member)
.orderBy(rankPath.desc())
.fetch();
for (Tuple tuple : result) {
String username = tuple.get(member.username);
Integer age = tuple.get(member.age);
Integer rank = tuple.get(rankPath);
System.out.println("username = " + username + " age = " + age + " rank = " + rank);
}
- Querydsl은 자바 코드로 작성하기 때문에 rankPath 처럼 복잡한 조건을 변수로 선언해서 select 절, orderBy 절에서 함께 사용할 수 있다.
결과
username = member4 age = 40 rank = 3
username = member1 age = 10 rank = 2
username = member2 age = 20 rank = 2
username = member3 age = 30 rank = 1
📌 상수, 문자 더하기
⚡ 상수 더하기
- 상수가 필요하면 Expressions.constant(xxx) 사용
Tuple result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetchFirst();
cf) 위와 같이 최적화가 가능하면 SQL에 constant 값을 넘기지 않는다.
상수를 더하는 것 처럼 최적화가 어려우면 SQL에 constant 값을 넘긴다.
⚡ 문자 더하기 concat
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
- 결과: member1_10
cf) member.age.stringValue() 부분이 중요한데,
문자가 아닌 다른 타입들은 stringValue() 로 문자로 변환할 수 있다. 이 방법은 ENUM을 처리할 때도 자주 사용한다.
김영한님의 <실전! Querydsl>을 수강하고 작성한 글입니다
![](https://t1.daumcdn.net/keditor/emoticon/friends1/large/047.gif)
PREV
[Querydsl] 2. 예제 도메인 모델
📌 엔티티 Member 엔티티 package study.querydsl.entity; import jakarta.persistence.*; import lombok.*; @Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString(of = {"id", "username", "age"}) public class Member { @Id @Ge
nyeroni.tistory.com
NEXT
[Querydsl] 4. 중급 문법
📌 프로젝션과 결과 반환 ✔ 프로젝션 : select 대상 지정 ⚡ 프로젝션 대상이 하나 List result = queryFactory .select(member.username) .from(member) .fetch(); 프로젝션 대상이 하나면 타입을 명확하게 지정할 수
nyeroni.tistory.com
'인프런 Spring 강의 정리 > 실전! Querydsl' 카테고리의 다른 글
[Querydsl] 6. 실무 활용 - 스프링 데이터 JPA와 Querydsl (1) | 2024.01.29 |
---|---|
[Querydsl] 5. 실무 활용 - 순수 JPA와 Querydsl (0) | 2024.01.28 |
[Querydsl] 4. 중급 문법 (1) | 2024.01.27 |
[Querydsl] 2. 예제 도메인 모델 (0) | 2024.01.27 |
[Querydsl] 1. 프로젝트 환경 설정 (1) | 2024.01.27 |