기본적으로 QueryDSL에 대한 설명은 위 엔티티 모델을 기준으로 한다.
기본 Q-Type 활용
- 우선 Querydsl을 사용하기 위해서는 EntityManager을 이용해 JPAQueryFactory를 생성해야 한다.
- 새로운 엔티티를 만들었을 경우 gradle에서 compileJava를 해주어야 한다.
- Q 객체 생성을 위함.
이제 기본적으로 Q-Type을 어떻게 사용해야 하는지 알아보자.
- Q클래스 인스턴스 사용 방법
1
2
QMember qMember = new QMember("m") //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
혹은 static import를 활용할 수도 있다.
1
2
3
Member findMember = queryFactory
.select(QMember.member)
//...
위와 같이도 사용할 수 있는데 이를 static import를 활용하면
1
2
3
4
5
Member findMember = queryFactory
.select(member)
.from(memeber)
.where(member.username.eq("memeber1"))
.fetchOne();
이렇게 바꿀 수 있다.
이러한 static import 방식을 제일 권장한다.
검색 조건 쿼리
우선 예시를 보자.
1
2
3
4
5
6
7
@Test
public void search() {
queryFactory.selectFrom(member)
.where(member.username.eq("member1")
.and(member.age.eq(10)))
.fetchOne();
}
- 검색 조건을 .and(), .or()를 메서드 체인으로 연결해 사용할 수 있다.
정리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
member.age.notIn(10, 20)
member.age.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%")
member.username.contains("member") // like '%member%'
member.username.startsWith("member") // like 'memeber%'
헷갈리더라도 IDE를 통해 빠르게 찾을 수 있다. 자바 코드라는 장점이 여기서도 발휘된다.
And 조건을 파라미터로 처리
1
2
3
4
5
6
7
8
9
10
@Test
public void searchAndParam() {
Member findMember = queryFactory.selectFrom(member)
.where(member.username.eq("member1"),
member.age.eq(10)
)
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
- where()에 파라미터로 검색 조건을 넣으면 and()를 사용한 것과 같은 효과.
- 이 경우 만약 null 값이 파라미터에 포함되면 null 값은 무시하기 때문에 동적 쿼리 생성에 장점이 있다.
결과 조회
- fetch(): 리스트 조회, 데이터가 없으면 빈 리스트 반환
- fetchOne(): 단 건 조회
- 결과X: null
- 결과 둘 이상이면: NonUniqueResultException 발생
- fetchFirst(): limit(1).fetchOne()
- fetchResults(): 페이징 정보를 포함, total count 쿼리 추가 실행
- fetchCount(): count 쿼리로 변경해 count 수 조회
1
2
3
4
5
6
7
8
9
10
11
12
QueryResults<Member> results = queryFactory
.selectFrom(member)
.fetchResults();
results.getTotal()
results.getLimit()
results.getOffset()
results.getResults() //결과 데이터
long count = queryFactory
.selectFrom(member)
.fetchCount();
정렬
1
2
3
4
5
6
7
8
9
10
11
12
@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();
}
- 회원 나이 내림차순
- 회원 이름 올림차순
- 단, 회원 이름이 없다면 마지막에 출력 (nullsLast)
result의 첫 번째 결과에는 member5가 들어간다.
나이가 제일 많기 때문이다.
두 번째 결과에는 member6이 들어간다. 나이가 똑같이 제일 많지만 이름 올림차순에서 밀렸기 때문이다.
세 번째 결과에는 이름이 없는 null, age=100 Member가 들어가게 되는 것이다.
- desc(), asc(): 일반 정렬
- nullsLast(), nullsFirst(): null 데이터 순서 부여
페이징
1
2
3
4
5
6
7
8
9
10
11
@Test
public void paging() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) //0부터 시작(zero index)
.limit(2) //최대 2건 조회
.fetch();
assertThat(result.size()).isEqualTo(2);
}
똑같이 0부터 시작한다.
전체 조회수가 필요하다면 fetchResults()를 사용하면 된다.
참고
실무에서 페이징 쿼리를 작성할 때 데이터를 조회하는 쿼리는 여러 테이블을 조회해야 하지만, count 쿼리는 조인이 필요 없는 경우도 있다.
그런데 fetchResults() 같은 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인을 해버리기 때문에 성능이 안나올 수 있다.
따라서 count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면 count 전용 쿼리를 별도로 작성해야 한다.
집합
집합 함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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);
}
- JPQL이 제공하는 모든 집합 함수를 제공한다.
- tuple은 Querydsl에서 제공하는 것이다.
- 사용 방법은 위의 예제 코드처럼 사용하면 된다.
- 보통은 DTO로 변환해 사용한다.
GroupBy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//팀의 이름과 각 팀의 평균 연령을 구하라.
@Test
public void group() {
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(member.age.avg())).isEqualTo(15);
}
- 그룹화된 결과에 조건을 걸어 제한하려면 똑같이 having을 사용하면 된다.
1
2
.groupBy(item.price)
.having(item.price.gt(1000))
조인
기본 조인
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭으로 사용할 Q타입을 지정하면 된다.
1
join(조인 대상, 별칭으로 사용할 Q타입)
1
2
3
4
5
6
7
//teamA에 소속된 모든 회원
queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
세타 조인
연관관계가 없는 필드로 조인
1
2
3
4
5
6
7
8
9
10
11
12
// 회원의 이름이 팀 이름과 같은 회원 조회하기.
@Test
public void theta_join() {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
queryFactory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
- from 절에 여러 엔티티를 선택해 세타 조인을 한다.
- 외부 조인 불가능
- on을 사용하면 외부 조인 가능
조인 - ON절
- ON절을 활용한 조인(JPA 2.1 부터 지원)
- 조인 대상 필터링
- 연관관계 없는 엔티티 외부 조인 (주로 이 경우 많이 사용)
조인 대상 필터링
1
2
3
4
5
6
7
8
9
//회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
@Test
public void join_on_filtering() {
queryFactory
.select(member, team)
.from(member)
.leftJoin(member.team, team).on(team.name.eq("teamA"))
.fetch();
1
2
3
4
5
6
// 결과
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]
연관관계 없는 엔티티 외부 조인
1
2
3
4
5
6
7
8
9
10
11
12
//회원의 이름과 팀의 이름이 같은 대상 외부조인
@Test
public void join_on_no_relation() {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
queryFactory
.select(member, team)
.from(member)
.leftJoin(team).on(member.username.eq(team.name))
.fetch();
- 일반 조인과 다르게 leftJoin() 부분에 엔티티가 하나만 들어가는 것을 볼 수 있다.
1
2
3
4
5
6
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)]
- team이름과 회원 이름이 같은 경우에만 team 정보를 가져왔다.
- left join이기 때문에 mebmer 데이터는 전부 가져온다.
페치 조인
페치 조인은 SQL에서 제공하는 기능이 아니다. SQL 조인을 활용해 연관된 엔티티를 SQL 한 번에 조회하는 기능일 뿐이다.
1
2
3
4
5
6
7
@Test
public void fetchJoinUse() {
queryFactory
.selectFrom(memeber)
.join(member.team, team).fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
페치조인을 사용하지 않는다면 지연 로딩으로 인해 경우에 따라 N + 1 문제가 발생할 것이다.
서브 쿼리
- com.querydsl.jpa.JPAExpressions를 사용한다.
쿼리 내부에 쿼리를 넣는 것.
eq 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//나이가 가장 많은 회원
@Test
public void subQuery() {
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);
}
- 별칭에 충돌이 일어나면 안되기 때문에 서브 쿼리의 QMember는 따로 선언해주었다.
goe 사용
1
2
3
4
5
6
7
8
9
10
//나이가 평균 나이 이상인 회원
queryFactory
.selectFrom(member)
.where(member.age.goe(
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
서브 쿼리 여러 건 처리 (in 사용)
1
2
3
4
5
6
7
8
9
queryFactory
.selectFrom(member)
.where(member.age.in(
JPAExpressions
.select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))
))
.fetch();
사용 방법만 보여줄 뿐 예제 자체는 억지이다.
select절에 서브쿼리 사용
1
2
3
4
5
6
7
queryFactory
.select(member.username,
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
).from(member)
.fetch();
from절의 서브 쿼리 한계
JPA JPQL 서브쿼리의 한계점으로 from절에는 서브쿼리를 지원하지 않는다.
이에 따라 Querydsl도 당연히 지원하지 않는다.
해결 방안
- 서브쿼리를 join으로 변경한다.
- 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
- nativeSQL을 사용한다.
Case문
- select, where, order by 에서 사용 가능
1
2
3
4
5
6
7
queryFactory
.select(memeber.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
1
2
3
4
5
6
7
queryFactory
.select(new CaseBuilder()
.when(member.age.between(0, 20)).then("0~20살")
.when(member.age.betwwen(21, 30)).then("21~30살")
.otherwies("기타")
.from(member)
.fetch();
응용
Querydsl은 자바 코드로 작성하기 때문에 보다 복잡한 조건을 변수로 선언해 select절, orderBy절에서 함께 사용할 수 있다.
- 0~30살이 아닌 회원을 가장 먼저 출력
- 후 0~20살 회원 출력
- 후 21살~30살 회원 출력
1
2
3
4
5
6
7
8
9
10
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();
이런식으로 활용 가능하다.
1
2
3
4
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) 사용
1
2
3
4
Tuple result = queryFactory
.select(memeber.username, Expressions.constant("A"))
.from(member)
.fetchFirst();
1
2
3
4
5
//결과
member1, A
member2, A
member3, A
문자
1
2
3
4
5
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
1
2
//결과
member1_10
팁
member.age.stringValue() 부분이 중요하다.
문자가 아닌 다른 타입들은 stringValue()로 문자로 변환이 가능하다.
이 방법은 보통 ENUM을 처리할 때 자주 사용하게 된다.