JPQL - 조인
개요
- JPQL도 SQL과 마찬가지고 조인을 지원한다.
- JPQL의 조인은 SQL의 조인과 기능이 대부분 같고, 문법만 약간 다르다.
바로 JPQL 조인에 대해 알아보자.
내부 조인
내부 조인 사용 예시
String teamName = "팀A";
//INNER JOIN 적용
String query = "SELECT m FROM Member m INNER JOIN m.team t "
+ "WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
.setParameter("teamName", teamName)
.getResultList();
INNER
는 생략 가능하다.
- 생성된 JPQL 쿼리
```xml
SELECT m FROM Member m INNER JOIN m.team t
WHERE t.name = '팀A'
```
- `FROM Member m`
- 회원을 선택하고 m이라는 별칭을 주었다.
- `Member m JOIN m.team t`
- 회원이 가지고 있는 **연관 필드로 팀과 조인**한다.
- 조인한 팀에는 t라는 별칭을 주었다. - 생성된 SQL 쿼리
```sql
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
WHERE
T.NAME='팀A'
```
JPQL 조인의 특징
- JPQL 조인 시, 연관 필드를 사용하여 조인한다.
- JPQL:
INNER JOIN m.team t
- SQL:
INNER JOIN TEAM T
- JPQL:
-
따라서 아래와 같은 JPQL 쿼리는 잘못된 쿼리이다.
SELECT m FROM Member m JOIN Team t WHERE t.name = '팀A'
조인한 두 개의 엔티티 동시 조회
-
아래 JPQL 쿼리를 작성하여 변수
query
에 저장했다고 가정하자.SELECT m, t FROM Member m JOIN m.team t
-
결과 자바 코드
List<Object[]> resultList = em.createQuery(query) .getResultList(); for (Object[] resultRecord : resultList) { Member member = (Member) resultRecord[0]; Team team = (Team) resultRecord[1]; }
외부 조인
외부 조인 사용 예시
-
외부 조인 JPQL 쿼리
SELECT m FROM Member m LEFT OUTER JOIN m.team t
OUTER
는 생략 가능하다.
-
변환된 외부 조인 SQL 쿼리
SELECT M.ID AS ID, M.AGE AS AGE, M.TEAM_ID AS TEAM_ID, M.NAME AS NAME FROM MEMBER M LEFT OUTER JOIN TEAM T ON M.TEAM_ID=T.ID
컬렉션 조인
컬렉션 조인이란?
- 일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라고 한다.
- 다대일 조인 vs 일대다 조인
- 다대일 조인
- [회원 → 팀]
- 단일 값 연관 필드 (
m.team
)을 사용하는 조인
- 일대다 조인
- [팀 → 회원]
- 컬렉션 값 연관 필드(
t.members
)를 사용하는 조인
- 다대일 조인
컬렉션 조인 사용 예시
-
컬렉션 조인을 사용하는 JPQL 쿼리
SELECT t, m FROM Team t LEFT JOIN t.members m
t LEFT JOIN t.members
- ‘팀’과 ‘팀이 보유한 회원목록’을 컬렉션 값 연관 필드로 외부 조인했다.
세타 조인
세타 조인이란?
- Full Join에서
WHERE
절로 조건을 제시하는 조인이다.- Full Join
- 각 테이블의 레코드 수를 곱한 만큼의 레코드를 출력하는 것
- 테이블 간에 연관관계가 전혀 없어도 상관없이 조인된다.
- 각 테이블의 레코드 간에 가능한 모든 수의 조합을 하여 결과로 반환한다.
- Full Join
- 세타 조인은 내부 조인만 지원한다.
세타 조인 사용 예시
-
회원 이름이 팀 이름과 똑같은 사람 수를 구하는 예시 쿼리 : JPQL
select count(m) from Member m, Team t where m.username = t.name
-
변환된 세타 조인 SQL 쿼리
SELECT COUNT(M.ID) FROM MEMBER M CROSS JOIN TEAM T WHERE M.USERNAME=T.NAME
JOIN ON 절
JOIN ON 절이란?
- JPA 2.1부터 지원하는 기능이다.
- ON 절을 사용하면, 조인 대상을 필터링하고 조인할 수 있다.
- 즉 ON 절의 조건으로 ‘원하는 조인 대상’을 걸러내고, 그에 대한 결과 테이블로 조인시킨다.
- 보통 ON 절은 외부 조인에서만 사용한다.
- 왜냐하면 내부 조인에서 ON 절을 사용하면, WHERE 절을 사용하는 것과 결과가 같기 때문이다.
JOIN ON 절 사용 예시
- 모든 회원을 조회하면서, 회원과 연관된 팀도 조회한다. 이때 팀은 이름이 ‘A’인 팀만 조회한다.
- 이에 대한 JPQL은 아래와 같다.
SELECT m, t FROM Member m LEFT JOIN m.team t ON t.name = 'A'
- 변환된 SQL은 아래와 같다.
SELECT m.*, t.* FROM MEMBER m LEFT OUTER JOIN TEAM t ON m.TEAM_ID=t.id AND t.name='A'
- SQL 결과를 보면, 조인시점에 조인 대상을
AND t.name='A'
로 필터링한다.
페치 조인
페치 조인이란?
- JPQL에서 성능 최적화를 위해 제공하는 기능이다.
- SQL에서 이야기하는 조인의 종류는 아니다.
- 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이 바로 페치 조인이다.
join fetch
명령어로 사용할 수 있다.-
페치 조인 문법
페치조인 ::= [LEFT [OUTER] | INNER] JOIN FETCH 조인경로
페치 조인에 대해 자세히 알아보자.
엔티티 페치 조인
-
페치 조인을 사용해서, 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회하는 JPQL
select m from Member m join fetch m.team
- 프로젝션 대상이
m
밖에 없다. join fetch
명령어를 사용했다.
- 프로젝션 대상이
- 위와 같이 JPQL 쿼리를 작성하면, 연관된 엔티티나 컬렉션을 함께 조회한다.
- 위 예시에선, 회원(
m
)과 팀(m.team
)을 함께 조회한다.
- 위 예시에선, 회원(
- 페치 조인은 별칭을 사용할 수 없다.
- 하지만 하이버네이트는 페치 조인에도 별칭을 허용한다.
-
위 JPQL 쿼리를 SQL로 변환한 결과
SELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
- JPQL에선 프로젝션 대상이
m
밖에 없었지만, SQL로 변환하니MEMBER
와TEAM
모두 프로젝션 대상이 되었다.
- JPQL에선 프로젝션 대상이
- 시각화
-
엔티티 페치 조인 시도 시
-
엔티티 페치 조인 결과 테이블
-
엔티티 페치 조인 결과 객체
- 회원과 팀 객체가 객체 그래프를 유지하면서 조회되었다.
-
- 상세 설명
- 엔티티 페치 조인 JPQL에서
select m
으로 회원 엔티티만 선택했는데, 실행된 SQL을 보면SELECT M.*, T.*
로 회원과 연관된 팀도 함께 조회된 것을 확인할 수 있다.- 즉 엔티티 페치 조인시, 연관 엔티티까지 가져온다.
- 엔티티 페치 조인 JPQL에서
- 엔티티 페치 조인 사용 예시
- 위에서 작성한 페치 조인 JPQL 쿼리를 사용해보자.
String jpql = "select m from Member m join fetch m.team"; List<Member> members = em.createQuery(jpql, Member.class) .getResultList(); for (Member member : members) { //페치 조인으로 회원과 팀을 함께 조회했다. //따라서 지연 로딩이 발생하지 않는다. System.out.println(member.getUsername() + "," + member.getTeam().getName()); }
-
출력 결과는 아래와 같다.
회원1,팀A 회원2,팀A 회원3,팀B
- 엔티티 페치 조인과 지연로딩
- 만약 회원과 팀을 지연로딩으로 설정했다고 가정해보자.
- 페치 조인을 사용했기 때문에, 회원을 조회할 때 팀도 함께 조회했다.
- 따라서, 연관된 팀 엔티티는 프록시가 아닌 실제 엔티티다.
- 즉 연관됨 팀을 사용해도 지연 로딩이 일어나지 않는다.
- 만약 회원과 팀을 지연로딩으로 설정했다고 가정해보자.
컬렉션 페치 조인
- 이번에는 일대다 관계인 컬렉션을 페치 조인해보자.
-
아래는 컬렉션 페치 조인 JPQL 쿼리이다.
select t from Team t join fetch t.members where t.name = '팀A'
- 페치 조인을 사용했다.
- 따라서 팀(
t
)를 조회하면서 연관된 회원 컬렉션(t.members
)도 함께 조회한다.
-
위 JPQL을 변환하여 실행된 SQL은 아래와 같다.
SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME='팀A'
- 시각화를 통해, 위 예시를 좀 더 자세히 알아보자.
-
컬렉션 페치 조인 시도
MEMBER
테이블과 조인하면서 결과가 증가한다.- 따라서 아래 조인 결과 테이블을 보면 같은 ‘팀A’가 2건 조회된다.
-
컬렉션 페치 조인 결과 테이블
- 같은 레코드
팀A
가 2개 존재한다.
- 같은 레코드
-
컬렉션 페치 조인 결과 객체
-
- 상세 설명
- JPQL에서
select t
로 팀만 선택했는데, SQL을 보면T.*, M.*
로 팀과 연관된 회원도 함께 조회한 것을 확인할 수 있다. TEAM
테이블과MEMBER
테이블이 조인하면서 결과가 증가했다.- 그로 인해, 조인 결과 테이블에 같은 ‘팀A’가 2건 조회되었다.
- 따라서 주소가
0x100
으로 같은 ‘팀A’를 2건 가지게 된다.
- JPQL에서
-
예시 코드
String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"; //아래 코드 실행 시, teams 리스트에 같은 객체(팀A)가 2개 들어간다. List<Team> teams = em.createQuery(jpql, Team.class).getResultList(); for (Team team : teams) { System.out.println(team.getName() + "," + team); for (Member member : team) { System.out.println("->" + member.getUsername() + "," + member); } System.out.println(); }
-
출력 결과는 아래와 같다.
팀A,Team@0x100 ->회원1,Member@0x200 ->회원2,Member@0x300 팀A,Team@0x100 ->회원1,Member@0x200 ->회원2,Member@0x300
- 같은 ‘팀A’가 2건 조회된 것을 확인할 수 있다.
- 그리고 연관된 엔티티 역시 동일하다.
-
페치 조인과 DISTINCT
DISTINCT
란?- SQL에서의
DISTINCT
- 중복된 결과를 제거하는 명령어
- JPQL에서의
DISTINCT
- SQL에
DISTINCT
를 추가한다. - 또한 애플리케이션에서 한 번 더 중복을 제거한다.
- SQL에
- SQL에서의
- 바로 위에서 살펴본 컬렉션 페치 조인은 ‘팀A’가 중복으로 조회된다.
DISTINCT
를 사용해서 이러한 문제를 해결할 수 있다.-
예시 JPQL 쿼리
select distinct t from Team t join fetch t.members where t.name = '팀A'
- SQL에
DISTINCT
가 추가된다. -
하지만 결과 테이블의 레코드 데이터가 다르므로, ‘SQL의
DISTINCT
‘로서의 효과는 없다. -
다음으로 애플리케이션에서
DISTINCT
명령어를 보고 중복된 데이터를 걸러낸다. -
이제 다시 결과를 출력해보자.
팀A,Team@0x100 ->회원1,Member@0x200 ->회원2,Member@0x300
- ‘팀A’ 객체의 중복이 사라졌다.
- SQL에
-
페치 조인과 일반 조인의 차이
- 일반 조인 예시
-
JPQL
select t from Team t join t.members m where t.name = '팀A'
-
SQL
SELECT T.* FROM TEAM T JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME='팀A'
-
- 일반 조인 시
- JPQL은 결과를 반환할 때, 연관관계까지 고려하지 않는다.
- 단지 SELECT 절에 지정한 엔티티 (프로젝션)만 조회할 뿐이다.
-
따라서 “회원 컬렉션을 지연 로딩으로 설정”하면 아래 그림과 같이, 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환한다.
- 또는 “회원 컬렉션을 즉시 로딩으로 설정”하면, 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한번 더 실행한다.
페치 조인의 특징과 한계
- 페치 조인의 특징
- 페치 조인을 사용하면, SQL 한번으로 연관된 엔티티들을 함께 조회할 수 있다.
- 따라서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다.
- 페치 조인은 글로벌 로딩 전략보다 우선한다.
- 즉 글로벌 로딩 전략을 지연 로딩으로 설정해도, JPQL에서 페치 조인을 사용하면 페치 조인을 적요해서 함께 조회한다.
- 연관된 엔티티를 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않는다.
- 따라서 준영속 상태에서도 객체 그래프를 탐색할 수 있다.
이미 연관된 엔티티 간의 객체 그래프가 생성되어 있기 때문에
- 페치 조인을 사용하면, SQL 한번으로 연관된 엔티티들을 함께 조회할 수 있다.
- 효과적인 설정방법
- 글로벌 로딩 전략 == 지연 로딩
- 최적화가 필요한 경우 == 페치 조인 적용
- 페치 조인의 한계
- 페치 조인 대상에는 별칭을 줄 수 없다.
- 하이버네이트는 페치 조인에 별칭을 지원하긴 하지만, 특별히 조심해서 사용해야한다.
자세한 것은 추후 포스팅으로 다룰 예정이다.
- 둘 이상의 컬렉션을 페치할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
- 왜냐하면 페치 조인과 함께 페이징 API를 사용하면, 메모리에서 페이징 처리를 하기 때문에 Overflow가 발생할 수 있다.
- 페치 조인 대상에는 별칭을 줄 수 없다.
참고
- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
- 반면에 여러 테이블을 조인해서, 엔티티가 가진 모양이 아닌 다른 결과를 내야 한다면 DTO로 반환하는 것이 더 효과적일 수 있다.
- 김영한, 『자바 ORM 표준 JPA 프로그래밍』, 에이콘