조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고,
두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.
@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");
}
👉 Querydsl에서는 JOIN 함수도 역시 제공된다.
join()
, innerJoin()
: 내부 조인(inner join)leftJoin()
: left 외부 조인(left outer join)rightJoin()
: right 외부 조인(right outer join)on
과 성능 최적화를 위한 fetch
조인 제공그렇다면 연관관계가 없는 엔티티 간의 조인은 어떻게 하는가 ❓ → 세타 조인
/**
* 세타 조인
* 팀 이름과 이름이 같은 회원 조회
*/
@Test
public void theta_join() throws Exception {
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
절에서 여러 엔티티를 선택해서 세타 조인이 가능하다.leftJoin
, rightJoin
)이 불가능한데 on
절을 사용해서 외부 조인이 가능하다.@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);
}
}
/* 실행 결과 */
/*
tuple = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
tuple = [Member(id=5, username=member3, age=30), null]
tuple = [Member(id=6, username=member4, age=40), null]
*/
on
절을 사용하여 조인 대상을 필터링할 때 내부 조인(inner Join)을 사용하면 where 절에서 필터링하는 것과 기능이 동일하다. 따라서 on 절을 활용한 조인 대상 필터링을 사용할 때, 내부 조인 이면 익숙한 where 절로 해결하고, 정말 외부 조인이 필요한 경우에만 이 기능을 사용하자..leftjoin(member.team, team).on(team.name.eq("teamA")) ->
.join(member.team, team).on(team.name.eq("teamA"))
@Test
public void join_on_no_relation() throws Exception {
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);
}
하이버네이트 5.1부터 on
절을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었다. 물론 내부 조인도 가능하다.
일반 조인과 다르게 leftJoin() 인자로 엔티티 하나만 들어간다.
leftJoin(member.team, team)
from(member).leftJoin(team).on(xxx)
페치 조인은 SQL 자체적으로 제공하는 기능은 아니고 JPA에서 SQL 조인을 활용해서 연관된 엔티티를 SQL 한 번에 조회해 가져오는 기능입니다.
성능 최적화를 위해 엔티티 연관관계에서 모든 로딩 전략을 지연 로딩(fetch = fetchType.LAZA)으로 설정하는데, 페치조인을 사용하면, getter 호출 시마다 쿼리를 수행하는 것을 막을 수 있습니다.
@PersistenceUnit
EntityManagerFactory emf;
@Test
public void fetchJoinNo() throws Exception {
em.flush();
em.clear();
Member member1 = queryFactory
.selectFrom(member)
.where(member.username.eq("member1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(member1.getTeam());
assertThat(loaded).as("페치 조인 미적용").isFalse();
}
/* 수행 쿼리 */
/*
select member1
from Member member1
where member1.username = 'member1'
*/
👉 그렇기 때문에 assertThat(loaded).as("페치 조인 미적용").isFalse(); 은 통과된다.
👉 즉시 로딩으로 Member, Team SQL 쿼리 조인으로 한 번에 조회
@Test
public void fetchJoinUse() throws Exception {
em.flush();
em.clear();
Member member1 = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(member1.getTeam());
assertThat(loaded).as("페치 조인 적용").isTrue();
}
join(member.team, team)
옆을 보면 .fetchJoin()
을 호출하는 것을 볼 수 있다.fetchJoin()
을 호출해 주면 연관관계에 있는 Team Entity도 함께 조회하기 때문에 loaded
가 true가 된 것을 확인할 수 있다.👉 com.querydsl.jpa.JPAExpressions
를 사용하여 서브 쿼리 사용이 가능하다.
@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);
}
@Test
public void subQueryGoe() throws Exception {
QMember mSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(
select(mSub.age.avg())
.from(mSub)
))
.fetch();
assertThat(result)
.extracting("age")
.containsExactly(30, 40);
}
@Test
public void subQueryIn() throws Exception {
QMember mSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.in(
select(mSub.age)
.from(mSub)
.where(mSub.age.gt(10))
))
.fetch();
assertThat(result)
.extracting("age")
.containsExactly(20, 30, 40);
}
@Test
public void selectSubQuery() throws Exception {
QMember mSub = new QMember("memberSub");
List<Tuple> fetch = queryFactory
.select(member.username,
select(mSub.age.avg())
.from(mSub)
)
.from(member)
.fetch();
for (Tuple tuple : fetch) {
System.out.println("tuple = " + tuple);
}
}
✅ from 절의 서브 쿼리 한계
👉 JPA JPQL 서브 쿼리의 한계점으로 from 절의 서브 쿼리(인라인 뷰)는 지원하지 않습니다.
JPQL 쿼리 빌더인 Querydsl 역시 같은 이유로 지원하지 않습니다.
하이버네이트 구현체를 사용하면 select 절의 서브 쿼리는 지원합니다. Querydsl도 하이버네이트 구현체를 사용하면 select 절 서브 쿼리를 지원합니다.
✅ from 절의 서브 쿼리 한계돌파 방안
JOIN
으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)nativeSQL
사용Querydsl에서 select, 조건절(where)에서 사용 가능
@Test
public void basicCase() throws Exception {
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() throws Exception {
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);
}
}
when
절 안에 조건이 들어갑니다. Expressions.constant(xxx)
사용@Test
public void constant() throws Exception {
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 값을 넘긴다
@Test
public void concat() throws Exception {
List<String> result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
✅ member.age.stringValue()
: 문자가 아닌 다른 타입(ex: int, float..)들을 문자로 변환해 준다.
이 방법은 ENUM을 처리할 때도 자주 사용한다.