Querydsl 수업을 듣고 정리한 내용입니다.
✔️ 집합 함수
/**
* JPQL * select * COUNT(m), //회원수
* SUM(m.age), //나이 합
* AVG(m.age), //평균 나이
* MAX(m.age), //최대 나이
* MIN(m.age) //최소 나이 * from Member m
*/@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);
}
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
을 사용할 수 있다.ex) groupBy(), having() 예시
.groupBy(item.price)
.having(item.price.gt(1000))
: item
의 price
를 기준으로 그룹핑을 하되, 가격이 1000
보다 큰 값만 그룹핑한다.
실행 결과
join(조인 대상, 별칭으로 사용할 Q타입)
/**
* 팀 A에 소속된 모든 회원
*/
@Test
public void join() {
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()
: 내부 조인leftJoin()
: left 외부 조인rightJoin()
: right 외부 조인on
과 성능 최적화를 위한 fetch
조인 제공
실행 결과
연관관계가 없는 필드로 조인
/**
* 세타 조인(연관관계가 없는 필드로 조인)
* 회원의 이름이 팀 이름과 같은 회원 조회
*/
@Test
public void theta_join() {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
em.persist(new Member("teamC"));
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
절을 활용한 조인 (JPA 2.1부터 지원)
1. 조인 대상 필터링
2. 연관관계 없는 엔티티 외부 조인
예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
* JPQL: select m, t from Member m left join m.team t on t.name = 'teamA'
* SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and
t.name='teamA'
*/@Test
public void join_on_filtering() {
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);
}
}
실행 결과
💡 참고
on 절
을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join
)을 사용하면,where 절
에서 필터링 하는 것과 기능이 동일하다. 따라서on 절
을 활용한 조인 대상 필터링을 사용할 때, 내부조인 이면 익숙한where 절
로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자.
예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
/**
* 2. 연관관계 없는 엔티티 외부 조인
* 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
*/
@Test
public void join_on_no_filtering() {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
em.persist(new Member("teamC"));
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("tuple = " + tuple);
}
}
on 절
을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었다. 물론 내부 조인도 가능하다.
⚠️ 문법을 주의
leftJoin()
부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
- 일반조인:
leftJoin(member.team, team)
- on조인:
from(member).leftJoin(team).on(xxx)
실행 결과
- 페치 조인은 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
은 조회하지 않는다.
실행 결과
즉시로딩으로
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);
}
✔️ 서브 쿼리 goe 사용
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> result = queryFactory
.select(member.username,
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
).from(member)
.fetch();
for (Tuple tuple : result) {
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;
@Test
public void selectSubQuery() {
QMember memberSub = new QMember("memberSub");
List<Tuple> result = queryFactory
.select(member.username,
select(memberSub.age.avg())
.from(memberSub))
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("username = " + tuple.get(member.username));
System.out.println("age = " + tuple.get(JPAExpressions.select(memberSub.age.avg()).from(memberSub)));
}
}
🌋 from절의 서브쿼리 한계
- JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.
- Querydsl도 지원하지 않는다.
- 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다.
- Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.
🏖 from 절의 서브쿼리 해결방안
1. 서브쿼리를 join으로 변경한다. → 가능할 때도, 불가능할 때도 있다.
2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
3. nativeSQL을 사용한다.
select
,조건절(where)
,order by
에서 사용 가능
✔️ 단순한 조건
@Test
public void basicCase() {
List<String> result = queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
age
가
10
이면 열살
출력20
이면 스무살
출력기타
출력
실행 결과
✔️ 복잡한 조건
@Test
public void complexCase() {
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();
for (String s : result) {
System.out.println("s = " + s);
}
}
CaseBuilder()
를 통해 동작한다.when
절 안에 조건이 들어간다.
실행 결과
✔️ orderBy에서 Case 문 함께 사용하기 예제
예를 들어서 다음과 같은 임의의 순서로 회원을 출력하고 싶다면?
1. 0 ~ 30살이 아닌 회원을 가장 먼저 출력
2. 0 ~ 20살 회원 출력
3. 21 ~ 30살 회원 출력
@Test
public void exampleCase(){
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);
}
}
rankPath
처럼 복잡한 조건을 변수로 선언해서 select 절
, orderBy 절
에서 함께 사용할 수 있다.
실행 결과
✔️ 상수가 필요하면 Expressions.constant(xxx)
사용한다.
@Test
public void constant() {
List<Tuple> result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
이와 같이 최적화가 가능하면 SQL에 constant
값을 넘기지 않는다.
상수를 더하는 것처럼 최적화가 어려우면 SQL에 constant
값을 넘긴다.
실행 결과
✔️ 문자 더하기 concat
@Test
public void concat() {
List<String> result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
member.age.stringValue()
부분이 중요하다.stringValue()
로 문자로 변환할 수 있다.ENUM
을 처리할 때도 매우 자주 사용한다.
실행 결과