JPQL의 조인은 SQL 조인과 실행되는 것은 똑같다.
차이가 뭐냐면 entity 중심으로 동작한다는 것이다. 조금 객체 스타일로 조인 문법이 나간다.
내부 조인:
SELECT m FROM Member m [INNER] JOIN m.team t
외부 조인:
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
세타 조인:
Select count(m) from Member m, Team t where m.username = t.name
내부, 외부, 세타 조인 한줄 설명
INNER JOIN(내부 조인): 두 테이블을 조인할 때, 두 테이블에 모두 지정한 열의 데이터가 있어야 한다.
OUTER JOIN(외부 조인): 두 테이블을 조인할 때, 1개의 테이블에만 데이터가 있어도 결과가 나온다.
THETA JOIN(세타조인):: 연관관계 상관 없이유저 명과 팀의 이름이 같은 경우 찾아라
라는 쿼리 날릴 수 있다.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
String query = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
tx.commit()
내부 조인 시 team이 존재하는 Member 정보만을 리턴한다.
실행 쿼리
Hibernate:
/* select
m
from
Member m
inner join
m.team t */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as team_id4_0_,
member0_.username as username3_0_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
String query = "select m from Member m left outer join m.team t";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
tx.commit()
외부 조인 시 team이 존재하지 않는 Member 정보도 함께 리턴한다.
이때 team이 존재하지 않다면 null로 나온다.
실행 쿼리
Hibernate:
/* select
m
from
Member m
left outer join
m.team t */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as team_id4_0_,
member0_.username as username3_0_
from
Member member0_
left outer join
Team team1_
on member0_.TEAM_ID=team1_.id
String query = "select m from Member m, Team t where m.username = t.name";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
두 테이블을 크로스 조인 후 조건에 해당하는 값만을 조회한다.
실행 쿼리
Hibernate:
/* select
m
from
Member m,
Team t
where
m.username = t.name */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as team_id4_0_,
member0_.username as username3_0_
from
Member member0_ cross
join
Team team1_
where
member0_.username=team1_.name
참고
하이버네이트 5.1부터 세타 조인도 외부 조인이 가능!
ON절을 활용한 조인(JPA 2.1부터 지원)
조인 대상 필터링
JPQL:
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
SQL:
SELECT m.*, t.* FROM
Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'
연관관계 없는 엔티티 외부 조인
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
연관관계가 전혀 없는 애를 LeftJoin 하고 싶다면 ON절에다가 명시해주면 된다.
페치 조인(Fetch Join)
페치 조인은 현업에서 굉장히 많이 쓰인다. fetchType을 LAZY로 다 세팅 해놓고, 쿼리 튜닝할때 한번에 조회가 필요한 경우 페치 조인을 사용한다.
엔티티 객체 그래프를 한번에 조회하는 방법이다.
회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)
JPQL
select m from Member m join fetch m.team
SQL
SELECT M.*, T.*
FROM MEMBER T
INNER JOIN TEAM T ON M.TEAM_ID = T.ID
최근에(jpa2.1)는 페치 조인 말고 엔티티 그래프라는 기능이 있다.
페치 조인 예시)
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("username = " + member.getUsername() + ","
+ "teamname = " + member.getTeam().name());
}
-----------------------------------------------------
Hibernate:
/* select
m
from
Member m
join
fetch m.team */ select
member0_.id as id1_0_0_,
team1_.id as id1_3_1_,
member0_.age as age2_0_0_,
member0_.TEAM_ID as team_id5_0_0_,
member0_.type as type3_0_0_,
member0_.username as username4_0_0_,
team1_.name as name2_3_1_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
member = 회원 1, teamA
member = 회원 2, teamA
member = 회원 3, teamB
일반 조인의 경우
String query = "select t from Team t join t.members m";
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
System.out.println("result.size() = " + result.size());
for (Team team : result) {
System.out.println("team = " + team.getName() + "|members=" + team.getMembers().size());
for (Member member : team.getMembers()) {
System.out.println("-> member = " + member);
}
}
----------------------------------------------
Hibernate:
/* select
t
from
Team t
join
t.members m */ select
team0_.id as id1_3_,
team0_.name as name2_3_
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
result.size() = 3
일반 조인의 경우 Select 절에서 Team만 가지고 온다. 일반 조인은 조인문만 SQL에서 실행되고 실제 데이터를 가져오는 것은 실제 사용될 때마다 가져오게 된다.(Lazy로 설정했기 때문에)
때문에 데이터가 뻥튀기 돼서 result.size() = 3
처럼 result의 개수가 3개로 나온 것을 알 수 있다.
문제는 뭐냐면 루프를 돌릴 때 켈렉션인 members가 아직 초기화가 안됐다는 것이다.
Hibernate:
select
members0_.TEAM_ID as team_id5_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.age as age2_0_1_,
members0_.TEAM_ID as team_id5_0_1_,
members0_.type as type3_0_1_,
members0_.username as username4_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
team = teamA|members=2
-> member = Member{id=3, username='회원 1', age=0}
-> member = Member{id=4, username='회원 2', age=0}
team = teamA|members=2
-> member = Member{id=3, username='회원 1', age=0}
-> member = Member{id=4, username='회원 2', age=0}
Hibernate:
select
members0_.TEAM_ID as team_id5_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.age as age2_0_1_,
members0_.TEAM_ID as team_id5_0_1_,
members0_.type as type3_0_1_,
members0_.username as username4_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
team = teamB|members=1
-> member = Member{id=5, username='회원 3', age=0}
일반 조인은 Select 절에서 Team만 가져오고 members는 초기화를 안했기 때문에 members가 사용되는 시점에 쿼리가 계속 나간다.
페치 조인
String query = "select t from Team t join fecth t.members";
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
System.out.println("result.size() = " + result.size());
for (Team team : result) {
System.out.println("team = " + team.getName() + "|members=" + team.getMembers().size());
for (Member member : team.getMembers()) {
System.out.println("-> member = " + member);
}
}
------------------------------------------------------
Hibernate:
/* select
t
from
Team t
join
fetch t.members m */ select
team0_.id as id1_3_0_,
members1_.id as id1_0_1_,
team0_.name as name2_3_0_,
members1_.age as age2_0_1_,
members1_.TEAM_ID as team_id5_0_1_,
members1_.type as type3_0_1_,
members1_.username as username4_0_1_,
members1_.TEAM_ID as team_id5_0_0__,
members1_.id as id1_0_0__
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
result.size() = 3
team = teamA|members=2
-> member = Member{id=3, username='회원 1', age=0}
-> member = Member{id=4, username='회원 2', age=0}
team = teamA|members=2
-> member = Member{id=3, username='회원 1', age=0}
-> member = Member{id=4, username='회원 2', age=0}
team = teamB|members=1
-> member = Member{id=5, username='회원 3', age=0}
페치 조인은 Selcet 절에서 데이터를 다 불러온다. 때문에 그 밑에서 지연 로딩이 발생하지 않는다.
페치 조인과 일반 조인의 차이
한계
String query = "select t from Team t join fetch t.members as m";
특징
정리
모든 것은 페치 조인으로 해결할 수는 없다.
페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
여러 테이블으 ㄹ조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
출처
자바 ORM 표준 JPA 프로그래밍 강의
게시글 속 자료는 모두 위 강의 속 자료를 사용했습니다.
JPA N+1 이슈는 무엇이고, 해결책은 무엇인가요?