기본 문법 - Part2
조인
기본 조인
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고,
두 번째 파라미터에 별칭으로 사용할 Q타입
을 지정하면 된다.
join(조인 대상, 별칭으로 사용할 Q타입)
• join()
, innerJoin()
: 내부 조인
• leftJoin()
: left 외부 조인
• rightJoin()
: right 외부 조인
▷ Querydsl
@Test
public void join() throws Exception {
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team) // leftJoin, rightJoin
.where(team.name.eq("teamA"))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("member1", "member2");
}
▷ JPQL
select member1
from Member member1
inner join member1.team as team
where team.name = 'teamA'
▷ SQL
select
--...
from member member0_
inner join team team1_
on member0_.team_id=team1_.team_id
where team1_.name=NULL;
참고로 inner join
을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다.
위의 쿼리를 보면 JPQL은 where절을, SQL은 on절을 사용한 걸 볼 수 있다.
세타 조인
연관관계가 없는 필드로 조인하는 방식이다.
@Test
public void theta_join() throws Exception {
em.persist(Member.of("teamA"));
em.persist(Member.of("teamB"));
// cross - join나간다.
List<Member> fetch = queryFactory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
assertThat(fetch)
.extracting("username")
.containsExactly("teamA", "teamB");
}
• from절에 여러 엔티티를 선택해서 세타조인을 할 수 있다.
• Outer Join은 불가능하다. → On절을 사용하면 가능해진다.
▷ JPQL
select
member1
from
Member member1, Team team
where
member1.username = team.name
▷ SQL
내부적으로 cross join
을 사용한다.
select
--...
from
member member0_
cross join team team1_
where member0_.username=team1_.name;
조인 - on절
ON절을 활용한 조인은 JPA 2.1
부터 지원하며 2가지 기능을 제공한다.
1. 조인 대상 필터링
2. 연관관계 없는 엔티티 외부 조인
조인 대상 필터링
▷ Querydsl
@Test
@DisplayName("조인 대상 필터링")
public void simpleJoinTest1() {
List<Tuple> result1 = queryFactory
.select(member,team)
.from(member)
.leftJoin(member.team, team).on(team.name.eq("teamA"))
.fetch();
List<Tuple> result2 = queryFactory
.select(member,team)
.from(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
}
▷ JPQL
--result1 Query
select
member1, team
from
Member member1
left join
member1.team as team
with team.name = 'teamA'
--result2 Query
select
member1, team
from
Member member1
inner join member1.team as team
where team.name = 'teamA'
▷ SQL
result2 Query의 가독성이 훨씬 좋다.
--result1 Query
select
--...
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
and (
team1_.name='teamA'
)
--result2 Query
select
--..
from
member member0_
inner join team team1_
on member0_.team_id=team1_.team_id
where team1_.name='teamA';
위에서도 잠깐 언급했지만 on절을 활용해 조인 대상을 필터링 할 때,
외부조인이 아니라 내부조인(inner join)을 사용하면, where절에서 필터링하는
것과 기능이 동일하다. 따라서 on절을 활용한 조인 대상 필터링을 사용할 때,
내부조인 이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만
이 기능을 사용하자.
참고
아래 두 블로그에 Join시 ON절과 WHERE절에서 필터링의 차이에 대해서 확인할 수 있다.
▷ Join시 ON절과 WHERE절에서 필터링의 차이1
▷ Join시 ON절과 WHERE절에서 필터링의 차이2
연관관계 없는 엔티티 외부 조인
▷ Querydsl
@Test
public void joinOnNoRelation() {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(team).on(member.username.eq(team.name))
.fetch();
}
여기서 주의할 점은 leftJoin()
의 문법을 잘 확인해야 한다.
leftJoin()
부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
일반 Join : leftJoin(member.team, team).on(~~)
On Join : leftJoin(team).on(~~)
▷ JPQL
select
member1, team
from
Member member1
left join Team team
with member1.username = team.name
▷ SQL
select
--..
from
member member0_
left outer join team team1_
on member0_.username=team1_.name;
조인 - fetch join
fetch join 적용하기
join()
,leftJoin()
뒤에 fetchJoin()
이라고 추가하면 된다.
@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();
assert findMember != null;
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).isTrue();
}
▷ JPQL
select
member1
from
Member member1
inner join fetch
member1.team as team
where
member1.username = 'member1'
▷ SQL
select
--..
from
member member0_
inner join team team1_
on member0_.team_id=team1_.team_id
where member0_.username='member1';
서브쿼리
서브 쿼리 eq 사용
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub)
))
.fetch();
▷ JPQL
select
m
from
Member m
where m.age = (
select max(memberSub.age)
from Member memberSub
)
▷ SQL
select
--..
from
member member0_
where
member0_.age=(
select
max(member1_.age)
from
member member1_
)
서브 쿼리 goe 사용
▷ Querydsl
import static com.querydsl.jpa.JPAExpressions.*;
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(
select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
▷ JPQL
select
m
from Member m
where m.age >= (
select avg(memberSub.age)
from Member memberSub
)
▷ SQL
select
--..
from
member member0_
where
member0_.age >= (
select
avg(cast(member1_.age as double))
from
member member1_
)
서브쿼리 여러 건 처리 in 사용
▷ Querydsl
List<Member> result = queryFactory
.selectFrom(member)
.where(member.in(
selectFrom(memberSub)
.where(memberSub.age.gt(10))
))
.fetch();
▷ JPQL
select
m
from Member m
where m.age in (
select memberSub
from Member memberSub
where memberSub.age > 10
)
▷ SQL
select
--..
from member member0_
where member0_.member_id in (
select member1_.member_id
from member member1_
where member1_.age>10
);
select 절에 subquery
▷ Querydsl
List<Tuple> result = queryFactory
.select(member.username,
select(memberSub.age.avg()).from(memberSub))
.from(member)
.fetch();
▷ JPQL
select
m
,(select avg(memberSub.age) from Member memberSub)
from Member m
▷ SQL
select
member0_.username as col_0_0_
,(select avg(cast(member1_.age as double))
from member member1_) as col_1_0_
from
member member0_
from 절의 서브쿼리
▷ 한계
JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.
당연히 Querydsl도 지원하지 않는다.
하이버네이트 구현체를 사용하면 select절의 서브쿼리는 지원한다.
Querydsl도 하이버네이트 구현체를 사용하면 select절의 서브쿼리를 지원한다.
▷ 해결방안
⒈ 서브쿼리를 join으로 변경한다.(가능한 상황도 있고, 아닐수도 있다.)
⒉ 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
⒊ nativeSQL을 사용한다.
CASE문
단순한 조건
▷ Querydsl
List<String> result = queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
▷ JPQL
select
case
when m.age = 10 then '열살'
when m.age = 20 then '스무살'
else '기타'
end
from Member m;
▷ SQL
select
case
when member0_.age=10 then '열살'
when member0_.age=20 then '스무살'
else '기타'
end as col_0_0_
from member member0_;
복잡한 조건
▷ Querydsl
List<String> result = queryFactory
.select(new CaseBuilder()
.when(member.age.between(0,20)).then("열살")
.when(member.age.between(21,30)).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
▷ JPQL
select
case
when (member1.age between 0 and 20) then '열살'
when (member1.age between 21 and 30) then '스무살'
else '기타'
end
from Member member1
▷ SQL
select
case
when member0_.age between 0 and 20 then '열살'
when member0_.age between 21 and 30 then '스무살'
else '기타'
end as col_0_0_
from member member0_;
orderBy에서 Case 문 함께 사용하기
▷ Querydsl
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();
▷ JPQL
select
member1.username,
member1.age,
case
when (member1.age between 0 and 20) then 2
when (member1.age between 21 and 30) then 1
else 3
end
from
Member member1
order by
case
when (member1.age between 0 and 20) then 2
when (member1.age between 21 and 30) then 1
else 3
end desc
▷ SQL
select
member0_.username as col_0_0_,
member0_.age as col_1_0_,
case
when member0_.age between 0 and 20 then 2
when member0_.age between 21 and 30 then 1
else 3
end as col_2_0_
from
member member0_
order by
case
when member0_.age between 0 and 20 then 2
when member0_.age between 21 and 30 then 1
else 3
end desc
상수, 문자 더하기
상수 더하기
▷ Querydsl
List<Tuple> result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
▷ JPQL
최적화가 가능한 경우에는 SQL에 constant값을 넘기지 않는다.
select
member1.username
-- constannt 값 없음.
from
Member member1
▷ SQL
select
member0_.username as col_0_0_
-- constannt 값 없음.
from
member member0_
문자 더하기
▷ Querydsl
문자가 아닌 다른 타입들은 stringValue()
메소드를 사용하여 문자로
변환할 수 있다. 이 방법은 ENUM을 처리할 때도 자주 사용된다.
List<String> result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.fetch();
▷ JPQL
select
concat(concat(member1.username,'_'), str(member1.age))
from
Member member1
▷ SQL
select
((member0_.username||'-')||cast(member0_.age as character varying)) as col_0_0_
from
member member0_