김영한님의 '실전! Querydsl'을 수강하며 정리하는 글입니다
on 절을 활용한 조인입니다.
예) 팀원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조회, 회원은 모두 조회
* JPQL: select m, t from Member m left join m.team t on t.name = 'teamA'
*/
@Test
public void join_on_filtering() {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
//.join(member.team, team)
//.where(team.name.eq("teamA"))
.leftJoin(member.team, team).on(team.name.eq("teamA"))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
만약 여기서 leftJoin이 아닌 그냥 join을 사용하면 결과는 팀 이름이 null값인 회원들은 조회하지 않게됩니다.
또한 on절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where절에서 필터링하는것과 기능이 동일합니다.
예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
/**
* 연관 관계 없는 엔티티 외부 조인
* 회원의 이름과 팀 이름이 같은 대상 외부 조인
* */
@Test
public void join_on_no_relation() {
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);
}
}
from(member).leftJoin(team).on(xxx)
페치 조인은 SQL에서 제공하는 기능은 아닙니다. SQL 조인을 활용해서 연관된 엔티티를 SQL로 한번에 조회하는 기능입니다. 주로 성능 최적화에 사용하는 방법입니다.
지연로딩으로 Member, Team SQL 쿼리 각각 실행
@PersistenceUnit
EntityManagerFactory emf;
@Test
public void fetchJoinNo() {
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() {
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
사용
밑에 예제들은 JPAExpressions를 static import한 상태입니다.
/**
* 나이가 가장 많은 회원 조회
*/
@Test
public void subQuery() {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
select(memberSub.age.max())
.from(memberSub)
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(40);
}
/**
* 나이가 평균 이상인 회원
*/
@Test
public void subQueryGoe() {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(
select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(30, 40);
}
@Test
public void subQueryIn() {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.in(
select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(20, 30, 40);
}
@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("tuple = " + tuple);
}
}
JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰), Querydsl를 지원하지 않습니다. 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원합니다. Querydsl도 하이버네이트 구현체를 사용하면 select절의 서브쿼리를 지원합니다.
from 절의 서브쿼리 해결방안
- 서브쿼리를 join으로 변경한다.(가능한 상황도 있고, 불가능한 상황도 있다)
- 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
- 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);
}
}
@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);
}
}
예를 들어 다음과 같은 순서로 회원을 출력하고 싶다면?
1. 0~30살이 아닌 회원을 가장 먼저 출력
2. 0~20살 회원 출력
3. 21~30살 회원 출력
@Test
public void orderByCase() {
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);
}
}
상수가 필요하면 Expressions.constant(xxx)
사용
Tuple result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetchFirst();
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
문자가 아닌 다른 타입들은 stringValue()
로 문자로 변환할 수 있다. ENUM을 처리할 때도 자주 사용한다.