FireDrago
[QueryDSL] 기본문법 2 본문
조인
기본조인
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고,
두 번째 파라미터에 별칭으로 사용할 Q 타입을지정하면 된다.
// join(조인 대상, 별칭으로 사용할 Q타입)
join(member.team, team)
/**
* 팀 A에 속한 모든 멤버
*/
@Test
void join() throws Exception {
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
// 별칭 team으로 사용
.where(team.name.eq("teamA"))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("member1", "member2");
}
| 명령어 | 설명 |
| join(), innerJoin() | 내부 조인 (inner join) |
| leftJoin() | left 외부 조인 (left outer join) |
| rightJoin() | right 외부 조인 (right outer join) |
세타조인
연관관계가 없는 필드로 조인
/**
* 세타 조인(연관관계가 없는 필드로 조인)
* 회원의 이름이 팀 이름과 같은 회원 조회
*/
@Test
void theta_join() throws Exception {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
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절
1. 조인 대상 필터링
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 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
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);
}
}
- on절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인을 사용하면
where절에서 필터링 하는 것과 기능이 동일하다. 따라서 on절을 활용한 조인대상 필터링을 사용할때,
내부조인 이면 익숙한 where절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자.
2. 연관관계 없는 엔티티 외부조인
/**
* 2. 연관관계 없는 엔티티 외부 조인
* 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
* JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
* SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name
*/
@Test
void join_on_no_relation() throws Exception {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
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("t = " + tuple);
}
}
- on을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었다.
물론 내부 조인도 가능하다.
- 일반조인 : leftJoin(member.team, team)
on조인 : from(member).leftJoin(team).on(xxx)
조인 - 페치조인
SQL조인을 활용해서 연관된 엔티티를 SQL 한번에 조회하는 기능이다. 성능 최적화에 사용하는 방법이다.
@Test
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();
}
- 실행된 SQL을 확인하면, 한번에 조회하는 것을 알 수 있다.
select
m1_0.id,
m1_0.age,
t1_0.team_id,
t1_0.name,
m1_0.username
from
member m1_0
join
team t1_0
on t1_0.team_id=m1_0.team_id
where
m1_0.username=?
서브쿼리
com.querydsl.jpa.JPAExpressions 사용
/**
* 나이가 가장 많은 회원 조회
*/
@Test
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);
}
/**
* select 절에 subquery
*/
@Test
void subQuerySelect() {
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)));
}
}
JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인뷰)는 지원하지 않는다.
당연히 Querydsl도 지원하지 않는다. 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다.
Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.
from절의 서브쿼리 해결방안
1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
3. nativeSQL을 사용한다.
case 문
select, 조건절(where), order by에서 사용 가능
// 단순한 조건
@Test
void caseSelect() 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
void caseDifficult() {
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);
}
}
orderBy에서 Case 문 함께 사용하기 예제
@Test
void caseRankPath() {
// Querydsl은 자바 코드로 작성하기 때문에 rankPath 처럼 복잡한 조건을 변수로 선언해서
// select 절, orderBy절에서 함께 사용할 수 있다.
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);
}
}
Querydsl은 자바 코드로 작성하기 때문에 rankPath처럼 복잡한 조건을 변수로 선언해서
select절, orderBy절에서 함께 사용할 수 있다.
상수,문자 더하기
상수가 필요하면 Expressions.constant(xxx) 사용
@Test
void constant() {
List<Tuple> result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
문자더하기 concat
@Test
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);
}
}
stringValue()로 문자로 변환할 수 있다. 이 방법은 ENUM 처리할 때도 자주 사용한다.
