-
#QueryDSL - 2 기본문법 - JOINQueryDSL 2021. 3. 30. 10:31
조인 - 기본 조인
기본 조인
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다
join(조인 대상, 별칭으로 사용할 Q타입)
기본 조인
/** * 팀 A에 소속된 모든 회원 */ @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"); }
· join() , innerJoin() : 내부 조인(inner join)
· leftJoin() : left 외부 조인(left outer join)
· rightJoin() : rigth 외부 조인(rigth outer join)
· JPQL의 on 과 성능 최적화를 위한 fetch 조인 제공 → 다음 on 절에서 설명
세타 조인
연관관계가 없는 필드로 조인
-연관관계가 없어도 조인을 할 수 있는 것!
/** * 세타 조인(연관관계가 없는 필드로 조인) * 회원의 이름이 팀 이름과 같은 회원 조회(정말 연관관계가 없는 조인을 하기 위해) */ @Test public 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절에 엔티티를 나열한다.
· where 절에 특정 조건을 적어주면 된다.
· 모든 멤버랑 모든 팀을 다 조인해 멤버와 팀의 이름이 같은 것을 조회
제약
· 외부 조인(left,right outer join) 불가능 → 조인 on을 사용하면 외부 조인 가능
조인 - on절
· ON절을 활용한 조인(JPA 2.1부터 지원)
1. 조인 대상 필터링
2. 연관관계 없는 엔티티 외부 조인
1. 조인 대상 필터링
예) 회원과 팀을 조인하면서, 팀 이름이 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() 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); } }
결과
t=[Member(id=3, username=member1, age=10), Team(id=1, name=teamA)] t=[Member(id=4, username=member2, age=20), Team(id=1, name=teamA)] t=[Member(id=5, username=member3, age=30), null] t=[Member(id=6, username=member4, age=40), null]
참고: on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, 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 public 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); } }
· 하이버네이트 5.1부터 on 을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었다.
물론 내 부 조인도 가능하다.
· 주의! 문법을 잘 봐야 한다. leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
· 일반조인: leftJoin(member.team, team)
- 이렇게 사용하면 아이디를 매칭하기때문에 조인하는 대상이 아이디로 매칭
· on조인: from(member).leftJoin(team).on(xxx)
- 아이디를 매칭안하기때문에 조인이 on절로만 매칭이된다.
결과
t=[Member(id=3, username=member1, age=10), null] t=[Member(id=4, username=member2, age=20), null] t=[Member(id=5, username=member3, age=30), null] t=[Member(id=6, username=member4, age=40), null] t=[Member(id=7, username=teamA, age=0), Team(id=1, name=teamA)] t=[Member(id=8, username=teamB, age=0), Team(id=2, name=teamB)]
조인 - 페치 조인
페치 조인은 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(); }
emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
결과가 이미 초기화(로딩)되 엔티티인디 아니면 초기화(로딩)가 안된 엔티티인지 알려주는 기능
페치 조인 적용
즉시로딩으로 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() 이라고 추가하면 된다.
참고: 페치 조인에 대한 자세한 내용은 JPA 기본편이나, 활용2편을 참고하자
서브 쿼리(쿼리 안에 쿼리)
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 사용
/** * 나이가 평균 나이 이상인 회원 */ @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
List<Tuple> fetch = queryFactory .select(member.username, JPAExpressions .select(memberSub.age.avg()) .from(memberSub) ).from(member) .fetch(); for (Tuple tuple : fetch) { System.out.println("username = " + tuple.get(member.username)); System.out.println("age = " + tuple.get(JPAExpressions.select(memberSub.age.avg()) .from(memberSub))); }
static import 활용
JPAExpressions 를 static import를 함으로서 코드를 간략화 시킬 수 있다.
import static com.querydsl.jpa.JPAExpressions.select; List<Member> result = queryFactory .selectFrom(member) .where(member.age.eq( select(memberSub.age.max()) .from(memberSub) )) .fetch();
from 절의 서브쿼리 한계
JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl 도 지원하지 않는다. 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. Querydsl도 하 이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.
from 절의 서브쿼리 해결방안
1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
3. nativeSQL을 사용한다. (JPA의 한계)
FROM절에 서브쿼리를 쓰는 안좋은 이유
- DB라는 것은 쿼리에 기능을 많이 제공하는데, SQL에 화면관련 로직과 추가 화면 꾸미는 용도 데이터가 들어가있으면
FROM절안에.. FROM절안에 ..FROM 이런식으로 들어갈 일이 많다.
SQL은 데이터를 가지고오는 것에 집중하고, 필요하면 Apllication에서 로직을 태우고, 화면에서 랜더링된 이쁜 데이터 포맷은 화면에서 해야한다. 날짜 이쁘게 나오고하는것!! 이것은 화면에서 해야한다. 이래야 쿼리의 재활용성이 생김.
어떻게든 화면에 맞춰서 쿼리를 할려고하면 SQL을 현대적 Application에 맞지 않는다. 쿼리만 복잡해지는 것.
DB는 데이터만 필터링하고 그룹핑해서 가져오고,로직은 Application이나 Presentration에서 끝내야한다.
DB는 말그대로 퍼올리는 것에 중점을 줘야하고 WHERE, GROUP BY 데이터를 최소화해서 가져오는 역할에 집중하면
FROM절 쿼리를 많이 줄일 수 있다.
'QueryDSL' 카테고리의 다른 글
#QueryDSL - 6 실무활용 <Spring Data JPA와 Querydsl> (0) 2021.04.04 #QueryDSL - 5 실무활용 <순수-JPA와 Querydsl> (0) 2021.04.01 #QueryDSL -3 - 기본문법 - Case문 / 상수,문자 더하기 (0) 2021.03.30 #QueryDSL -1 기본 Q-Type 활용 (0) 2021.03.29 #QueryDSL - 0 (0) 2021.03.29