select m.username -> 상태 필드
fromMember m
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name ='팀A'
점을 찍어서 객체 그래프를 탐색할 수 있는 식을 경로 표현식이라고 한다.
상태 필드(state field): 단순히 값을 저장하기 위한 필드, 경로 탐색의 끝이며 더이상 탐색 X (불가)
연관 필드(association field): 연관 관계를 위한 필드
단일 값 연관 경로
대상이 엔티티인 경우를 말한다. ex) @ManyToOne, @OneToOne
묵시적 내부 조인 발생(join키워드 직접 사용), 탐색 O
컬렉션 값 연관 경로
대상이 컬렉션인 경우, ex) @ManyToMany, @OneToMany
묵시적 내부 조인 발생, 탐색 X
FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
실무에서 명시적 조인을(JOIN 키워드 직접 사용) 써야 덜 헷갈리고 에러가 발생하지 않는다.
Ex) 단일 값 연관 경로
java
열기
package jpql;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member1 = new Member();
member1.setUsername("관리자1");
em.persist(member1);
Member member2 = new Member();
member2.setUsername("관리자2");
em.persist(member2);
em.flush();
em.clear();
String query = "SELECT m.team FROM Member m";
List<Team> result = em.createQuery(query, Team.class).getResultList();
for (Team s : result) {
System.out.println("s = " + s);
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
묵시적 내부 조인이 발생
실무에서 이렇게 발생하도록 짜지 않는다.
java
열기
/* SELECT
m.team
FROM
Member m */ select
team1_.id as id1_3_,
team1_.name as name2_3_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
Ex) 컬렉션 값 연관 경로
java
열기
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member1 = new Member();
member1.setUsername("관리자1");
em.persist(member1);
Member member2 = new Member();
member2.setUsername("관리자2");
em.persist(member2);
em.flush();
em.clear();
String query = "SELECT t.members FROM Team t";
Collection result = em.createQuery(query, Collection.class).getResultList();
for (Object o : result) {
System.out.println("o = " + o);
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
값이 없어서 출력은 안 된다.
t.members 뒤에 점을 찍으면 뭔가 추천되는 값이 없다. 컬렉션 자체를 가리키기 때문이다.
java
열기
/* SELECT
t.members
FROM
Team t */ select
members1_.id as id1_0_,
members1_.age as age2_0_,
members1_.TEAM_ID as TEAM_ID5_0_,
members1_.type as type3_0_,
members1_.username as username4_0_
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
Ex) 상태 필드 - inner join 없음
java
열기
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team team = new Team();
em.persist(team);
Member member1 = new Member();
member1.setUsername("관리자1");
member1.setTeam(team);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("관리자2");
member2.setTeam(team);
em.persist(member2);
em.flush();
em.clear();
String query = "SELECT t.members.size FROM Team t";
Integer result = em.createQuery(query, Integer.class).getSingleResult();
System.out.println("res = " + result);
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
java
열기
/* SELECT
t.members.size
FROM
Team t */ select
(select
count(members1_.TEAM_ID)
from
Member members1_
where
team0_.id=members1_.TEAM_ID) as col_0_0_
from
Team team0_
res = 2
Ex) 컬렉션 값 연관 경로
그런데 t.members 뒤에는 더이상 탐색이 안된다.
따라서, 명시적 조인을 써야한다. 별칭을 설정하면 별칭을 통해 탐색 가능하기 때문이다.
권장: SELECT m FROM Team t JOIN t.members m
java
열기
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team team = new Team();
em.persist(team);
Member member1 = new Member();
member1.setUsername("관리자1");
member1.setTeam(team);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("관리자2");
member2.setTeam(team);
em.persist(member2);
em.flush();
em.clear();
String query = "SELECT t.members FROM Team t";
Collection result = em.createQuery(query, Collection.class).getResultList();
System.out.println("res = " + result);
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
java
열기
/* SELECT
t.members
FROM
Team t */ select
members1_.id as id1_0_,
members1_.age as age2_0_,
members1_.TEAM_ID as TEAM_ID5_0_,
members1_.type as type3_0_,
members1_.username as username4_0_
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
res = [Member{id=2, username='관리자1', age=0}, Member{id=3, username='관리자2', age=0}]
명시작 조인, 묵시적 조인
명시적 조인: "JOIN" 키워드를 직접 사용
묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생한다. (내부 조인만 가능)
외부 조인하려면 명시적 조인을 해야한다.
Ex) 경로 표현식
컬렉션은 자기 자신 또는 size() 까지만 가져올 수 있고 더 이상 탐색은 불가능하다.
sql
닫기
select o.member.team
fromOrder o -> 성공
select t.members from Team -> 성공
select t.members.username from Team t -> 실패
select m.username from Team t join t.members m -> 명시적 조인이라서 성공
경로 탐색을 사용한 묵시적 조인 시 주의사항
묵시적 조인은 항상 내부 조인
컬렉션은 경로 탐색의 끝이다. 별칭 얻어야 탐색을 이어갈 수 있다.
경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 준다.
[JPQL]
select m fromMember m joinfetch m.team
[SQL]
SELECT M.*, T.*FROMMEMBER M
INNERJOIN TEAM T ON M.TEAM_ID=T.ID
FK => PK (MEMBER.TEAM_ID -> TEAM.ID)
결과
Ex) 페치 조인이 나오게 된 이유
java
열기
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "SELECT m FROM Member m";
List<Member> result = em.createQuery(query, Member.class).getResultList();
for (Member member : result) {
System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
LAZY 설정되어 있어서 프록시로 들어오고 getName() 호출한 시점에야 데이터베이스에 쿼리를 날린다.
쿼리가 너무 많이 나온다. "N + 1" 문제
LAZY, EAGER 둘다 "N + 1" 문제가 발생한다
fetch join으로 해결 가능
java
열기
/* SELECT
m
FROM
Member m */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as TEAM_ID5_0_,
member0_.type as type3_0_,
member0_.username as username4_0_
from
Member member0_
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
member = 회원1, 팀A
member = 회원2, 팀A
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
member = 회원3, 팀B
Ex) fetch join - ManyToOne
java
열기
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "SELECT m FROM Member m join fetch m.team";
List<Member> result = em.createQuery(query, Member.class).getResultList();
for (Member member : result) {
System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
join으로 한방 쿼리가 나간다. 멤버와 팀 inner join
for문에서 getTeam()으로 얻는 team은 프록시가 아니다.
createQuery() 실행되는 시점에 얻은 팀은 fetch join으로 얻은 실제 엔티티(프록시 아님)
지연로딩 없이 깔끔하게 나온다.
java
열기
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, 팀A
member = 회원2, 팀A
member = 회원3, 팀B
컬렉션 페치 조인
일대다 관계
java
닫기
[JPQL]
select t
from Team t join fetch t.members
where t.name = ‘팀A'
[SQL]
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
Ex) 컬렉션 페치 조인 시 주의사항
java
열기
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "SELECT t FROM Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class).getResultList();
for (Team team : result) {
System.out.println("team = " + team.getName() + ", member: " + team.getMembers().size() + "명");
for(Member member : team.getMembers()){
System.out.println(" => member = " + member);
}
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
java
열기
/* SELECT
t
FROM
Team t
join
fetch t.members */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
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀B, member: 1명
=> member = Member{id=5, username='회원3', age=0}
팀A 내용이 왜 2개가 출력될까?
데이터베이스 특성 때문에 일대다 조인은 여러 개 쿼리가 실행될 수 있다.
팀, 멤버 테이블을 조인하는데 두 줄이 나온다. (멤버의 pk가 다르니까)
JPA는 이 시점에 개입 불가능하다.
Ex) FETCH JOIN이 있는 경우와 없는 경우의 result size 차이
JOIN FETCH 없을 때 => result size: 2
java
열기
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "SELECT t FROM Team t";
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() + ", member: " + team.getMembers().size() + "명");// for(Member member : team.getMembers()){// System.out.println(" => member = " + member);// }// }
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
JOIN FETCH 있을 때 => result size = 3
java
열기
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "SELECT t FROM Team t join fetch 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() + ", member: " + team.getMembers().size() + "명");// for(Member member : team.getMembers()){// System.out.println(" => member = " + member);// }// }
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
위 결과가 왜 다를까?
조인하면서 데이터가 커지기 때문이다.
일대다 조인하면 데이터가 더 커질 수 있다는 특성 때문에 이런 현상이 생긴다.
페치 조인과 DISTINCT
SQL의 DISTINCT는 중복된 결과를 제거하는 명령이다.
JPQL의 DISTINCT는 2가지 기능 제공
SQL에 DISTINCT 추가
애플리케이션에서 엔티티의 중복을 제거한다.
DISTINCT가 추가로 애플리케이션에서 중복 제거를 시도한다.
같은 식별자를 가진 Team 엔티티를 제거한다.
Ex) DISTINCT
SQL에 DISTINCT를 추가하지만 데이터가 다르기 때문에 SQL 결과에서 중복 제거 실패한다.
java
닫기
selectdistinct t
from Team t joinfetch t.members
where t.name = ‘팀A’
Ex) DISTINCT
java
열기
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "SELECT DISTINCT t FROM Team t join fetch 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() + ", member: " + team.getMembers().size() + "명");
for(Member member : team.getMembers()){
System.out.println(" => member = " + member);
}
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
컬렉션에서 중복을 제거해준다. => size = 2
java
열기
/* SELECT
DISTINCT t
FROM
Team t
join
fetch t.members */ select
distinct 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 : 2
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀B, member: 1명
=> member = Member{id=5, username='회원3', age=0}
페치 조인과 일반 조인의 차이
일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않는다.
JPQL은 결과를 반환할 때, 연관 관계를 고려하지 않는다.
단지, SELECT 절에 지정한 엔티티만 조회할 뿐이다.
아래 예제에서 팀 엔티티만 조회하고 회원 엔티티는 조회하지 않는다.
페치 조인을 사용할 때만 연관된 엔티티도 함께 조회한다. (즉시 로딩)
페치 조인을 사용하면 즉시 로딩이 일어난다고 보면 된다.
페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념이다. 한 방 쿼리
java
닫기
[JPQL]
select t
from Team t join t.members m
where t.name = ‘팀A'
[SQL]
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
Ex) 페치 조인과 일반 조인의 차이
일반 조인
java
열기
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "SELECT t FROM Team t join 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() + ", member: " + team.getMembers().size() + "명");
for(Member member : team.getMembers()){
System.out.println(" => member = " + member);
}
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과 - 일반 조인
조인했는데 SELECT절에서 팀 id랑 name만 나온다. (윗 부분 JOIN)
데이터 늘어나서 3으로 출력된다.
for문 돌릴 때, members가 초기화되지 않는 상태이다.
컬렉션이 프록시는 아니지만 데이터가 없어서 쿼리가 쭉 나온다. (맨 아래)
java
열기
/* SELECT
t
FROM
Team t
join
t.members */ 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
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 = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀A, member: 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 = 팀B, member: 1명
=> member = Member{id=5, username='회원3', age=0}
fetch join 추가
java
열기
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "SELECT t FROM Team t join fetch 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() + ", member: " + team.getMembers().size() + "명");
for(Member member : team.getMembers()){
System.out.println(" => member = " + member);
}
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과 - fetch join
일반 조인과 다르게 맨 처음 SELECT 절에서 member.id, name 뿐만 아니라 멤버에 대한 내용도 모두 가져온다.
나중에 for문을 돌려서 getName(), getMember()를 출력하면 한 번에 멤버 정보를 가져온다.
java
열기
/* SELECT
t
FROM
Team t
join
fetch t.members */ 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 = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀B, member: 1명
=> member = Member{id=5, username='회원3', age=0}
11.3 페치 조인 2 - 한계
페치 조인 특징과 한계
페치 조인 대상에는 원칙적으로 별칭을 줄 수 없다.
하이버네이트는 가능
관례적으로 별칭을 쓰지 않도록 한다. (페치 조인을 여러 단계로 하는 경우는 가끔 별칭을 쓰기도 한다.)
가급적 사용 X
페치 조인은 연관된 것들을 모두 가져오는 기능이다.
ex) 팀 안에 5명이 있는데 그 중 3 명만 조회하는 경우? => 위험하다. (지워지거나 오작동 가능성)
사상이 안 맞는 "정합성(무모순성, Data Consistency)" 이슈 때문
이런 경우는 처음부터 5명 조회하는 SELECT 5명 쿼리를 날려야한다.
둘 이상의 컬렉션을 페치 조인할 수 없다.
이 부분도 데이터 정합성 이슈가 있다.
ex) Team의 orders?
일대다도 데이터 뻥튀기 되는데 이 경우는 일대다"대"다 => 데이터가 예상치 못하게 뻥튀기 가능하다.
컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults) 사용 불가
일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능 (데이터 뻥튀기가 안 되서 가능)
하이버네이트는 경고 로그를 남기고 메모리에서 페이징 (매우 위험)
페이징은 철저히 데이터베이스 중심적이다. rows를 줄이려는 기능
연관된 엔티티들은 SQL 한 번으로 조회된다. => 성능 최적화
페치 조인이 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한다.
@OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
실무에서 글로벌 로딩 전략은 모두 지연 로딩이다.
최적화 필요한 곳에만 페치 조인 적용하면 대부분 성능 문제가 해결된다.
페치 조인 정리
모든 걸을 페치 조인으로 해결할 수는 없다.
페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야하면 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터만 조회해서 DTO로 반환하는 것이 효과적이다.
Ex) 컬렉션 페치조인과 페이징 API
java
열기
List<Team> result = em.createQuery(query, Team.class)
.setFirstResult(0) // 조회 시작할 위치
.setMaxResults(1) // 조회할 데이터 수
.getResultList();
Note) 실행 결과
DB에서 팀에 대한 데이터를 다 끌고 온다. 장애 나기 좋은 조건이다.
java
열기
Hibernate:
/* SELECT
t
FROM
Team t
join
fetch t.members */ 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 : 1
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
경고 - 페치조인과 페이징
java
닫기
WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
Ex) 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
기존의 쿼리
String query = "SELECT t FROM Team t join fetch t.members";
바뀐 쿼리 - 멤버부터 시작
String query = "SELECT m FROM Member m join fetch m.team t";
Note) 실행 결과 - 회원 => 팀 다대일은 페이징에 문제가 없다.
Ex) BatchSize를 쓰게 된 배경
BatchSize는 하위 엔티티를 로딩할 때, 한번에 상위 엔티티 ID를 지정한 숫자만큼 in Query로 로딩한다.
java
열기
String query = "SELECT t FROM Team t";
List<Team> result = em.createQuery(query, Team.class)
.setFirstResult(0)
.setMaxResults(2)
.getResultList();
Note) 실행 결과
lazy 로딩이 2번 일어난다. 성능 안 좋다.
팀을 10개 조회하면 연관된 멤버를 조회하는데 추가적으로 쿼리가10번 이상 나갈 것이다.
해결 방법: @BatchSize()
java
열기
/* SELECT
t
FROM
Team t */ select
team0_.id as id1_3_,
team0_.name as name2_3_
from
Team team0_ limit ?
result size : 2
-- 여기부터 중요
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 = 팀A, member: 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 = 팀B, member: 1명
=> member = Member{id=5, username='회원3', age=0}
Ex) @BatchSize
팀 클래스
java
열기
@OneToMany(mappedBy = "team")@BatchSize(size = 100)private List<Member> members = new ArrayList<>();
Main 클래스
java
열기
String query = "SELECT t FROM Team t";
List<Team> result = em.createQuery(query, Team.class)
.setFirstResult(0)
.setMaxResults(2)
.getResultList();
Note) 실행 결과
사이즈는 2개로 정상적으로 나온다.
그런데 그 아래 쿼리를 보면?
아래의 select에서 멤버를 가져오는데 in Query 안에 "?, ?"라고 나온다. TeamA, TeamB와 연관된 내용을 모두 가져온 것이다.
(기존에 @BatchSize를 쓰기 전에는 @ where members0_.TEAM_ID=? 이었다.)
팀에서 멤버스를 가져올 때, LAZY 로딩 상태이다.
매인 클래스에서 List<>에 담긴 Team을 in Query로 100 개씩 넘긴다.
쿼리 하나 + lazy 로딩 쿼리 n개 => n + 1 문제
페치 조인 또는 batchSize() 로 해결한다.
java
열기
/* SELECT
t
FROM
Team t */ select
team0_.id as id1_3_,
team0_.name as name2_3_
from
Team team0_ limit ?
result size : 2
Hibernate:
/* load one-to-many jpql.Team.members */ select
members0_.TEAM_ID as TEAM_ID5_0_1_,
members0_.id as id1_0_1_,
members0_.id as id1_0_0_,
members0_.age as age2_0_0_,
members0_.TEAM_ID as TEAM_ID5_0_0_,
members0_.type as type3_0_0_,
members0_.username as username4_0_0_
from
Member members0_
where
members0_.TEAM_ID in(
?, ?
)
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀B, member: 1명
=> member = Member{id=5, username='회원3', age=0}
[JPQL]
select i from Item i
where type(i)IN(Book, Movie)
[SQL]
select i from i
where i.DTYPE in(‘B’, ‘M’)
TREAT (JPA 2.1)
상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. (Java의 타입 캐스팅과 유사)
FROM, WHERE, SELECT(하이버네이트 지원) 사용
Ex) TREAT
부모 Item, 자식 Book
다운 캐스팅 처럼 사용
java
닫기
[JPQL]
select i from Item i
where treat(i as Book).auther = ‘kim’
[SQL]
select i.* from Item i
where i.DTYPE = ‘B’ and i.auther = ‘kim’
11.5 JPQL 엔티티 직접 사용
엔티티 직접 사용 - 기본 키 값
JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
COUNT() 안에 멤버의 기본 키를 직접 넣어 버린다.
비교하기 위한 값은 PK니까 ID가 매개변수로 들어간다.
java
닫기
[JPQL]
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용
[SQL](JPQL 둘다 같은 다음 SQL 실행)
select count(m.id) as cnt from Member m
Ex) 엔티티를 파라미터로 전달하나 식별자를 전달하나 결과는 동일
java
열기
[엔티티를 파라미터로 전달]
String jpql = “select m from Member m where m = :member”;
List resultList = em.createQuery(jpql)
.setParameter("member", member)
.getResultList();
[식별자를 직접 전달]
String jpql = “select m from Member m where m.id = :memberId”;
List resultList = em.createQuery(jpql)
.setParameter("memberId", memberId)
.getResultList();
[실행된 SQL]
select m.* from Member m where m.id=?
엔티티 직접 사용 - 외래 키 값
Ex)
아래에서 where절에 m.team: 외래 키, 파라미터 team은 기본 키
java
열기
Team team = em.find(Team.class, 1L);
String qlString = “select m from Member m where m.team = :team”;
List resultList = em.createQuery(qlString)
.setParameter("team", team)
.getResultList();
String qlString = “select m from Member m where m.team.id = :teamId”;
List resultList = em.createQuery(qlString)
.setParameter("teamId", teamId)
.getResultList()
[실행된 SQL]
select m.* from Member m where m.team_id=?
11.6 Named 쿼리
Named 쿼리 - 정적 쿼리
엔티티에 이름이 부여된 쿼리를 미리 등록해두고 사용하는 기능을 말한다. 쿼리를 재활용한다.
정적 쿼리만 가능하다.
애너테이션이나 XML에 Named 쿼리를 정의한다.
애플리케이션 로딩 시점에 초기화 후, 재사용한다.
정적 쿼리는 변하지 않는다. 따라서, 로딩 시점에 쿼리를 SQL로 파싱하고 캐시한다.
JPQL는 기본적으로 SQL로 변환한 다음에 실행되는데 네임드 쿼리를 로딩 시점에 딱 한 번 실행되고 캐시되니까 비용이 거의 없다고 봐도된다.
애플리케이션 로딩 시점에 쿼리를 검증한다.
Ex) Named 쿼리 - 애너테이션
Member 클래스
여기에 쿼리를 잘못 입력하면 SQLsyntax 에러가 난다.
클래스 이름을 앞에 붙여주는 게 관례
java
열기
@Entity@NamedQuery(
name = "Member.findByUsername",
query = "SELECT m FROM Member m WHERE m.username = :username"
)publicclassMember{
...
}
/* Member.findByUsername */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as TEAM_ID5_0_,
member0_.type as type3_0_,
member0_.username as username4_0_
from
Member member0_
where
member0_.username=?
member = Member{id=3, username='회원1', age=0}
Named 쿼리 - XML에 정의
persistence에 ormMember라는 mapping파일에 넣는다.
아래와 같이 데이터를 넣어주면 네임드 쿼리를 불러올 때, 애너테이션에서 불러올 수도 있고 ormMember라는 xml 파일에서도 불러올 수 있다.
xml이 항상 우선권을 가진다.
애플리케이션 운영 환경에 따라 다른 XML을 배포 가능
java
닫기
[META-INF/persistence.xml]
<persistence-unit name="jpabook" >
<mapping-file>META-INF/ormMember.xml</mapping-file>
[META-INF/ormMember.xml]
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
<named-query name="Member.findByUsername">
<query><![CDATA[
select m
from Member m
where m.username = :username
]]></query>
</named-query>
<named-query name="Member.count">
<query>select count(m) from Member m</query>
</named-query>
</entity-mappings>
cf) Named 쿼리 - JpaRepository
스프링 jpa를 쓰면 JpaRepository를 상속 받은 인터페이스의 메서드 위에 @Query()를 선언하고 괄호 안에 Named 쿼리를 쓸 수 있다.
jpa가 이를 네임드 쿼리로 등록한다. 애플리케이션 로딩 시점에 파싱도 하고 오류도 잡아 낸다. - 큰 장점
위의 예제대로 쓰는 것보다 이 방법이 더 좋다.
11.7 벌크(bulk) 연산
벌크 연산
ex) 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
JPA 변경 감지 기능으로 실행하려면 SQL이 너무 많이 실행된다.
재고가 10개 미만인 상품을 리스트로 조회
상품 엔티티의 가격을 10% 증가한다.
트랜잭션 커밋 시점에 변경 감지가 동작한다.
변경된 데이터가 100건이라면 100번의 UPDATE SQL이 실행된다.
벌크 연산 예제
쿼리 한 번으로 여러 테이블(엔티티)의 로우를 일괄적으로 변경한다. (엔티티)
executeUpdate(): 쿼리를 실행하고 나서 영향 받은 엔티티의 수를 반환한다.
UPDATE, DELETE 지원한다.
INSERT(하이버네이트는 insert into ... SELECT 지원)
벌크 연산 주의 사항
벌크 연산은 영속성 컨텍스에 저장하는 과정을 생략하고 데이터베이스에 직접 쿼리한다.
데이터 정합성에 어긋날 수 있다.
아래 두 가지 중 선택해서 쓴다.
벌크 연산을 먼저 실행한다.
벌크 연산 수행 후, 영속성 컨텍스트를 초기화한다. em.clear().
Ex) 벌크 연산
초기화를 안 화면?
java
열기
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
member1.setAge(0);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
member2.setAge(0);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
member3.setAge(0);
em.persist(member3);
em.flush();
em.clear();
// flush 되는 시점 (DB에 Age가 0으로 들어간다. 영속성 컨텍스트에서 지워지는 건 아니다.)int resultCnt = em.createQuery("UPDATE Member m set m.age = 20") // 그런데, DB에는 20으로 업데이트
.executeUpdate();
System.out.println("resultCnt = " + resultCnt);
System.out.println("member1.getAge() = " + member1.getAge());
System.out.println("member2.getAge() = " + member2.getAge());
System.out.println("member3.getAge() = " + member3.getAge());
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
createQuery()에서 설정한대로 나이가 20살이 들어가지 않는다.
초기화 em.clear()를 안했기 때문이다.
초기화하면 영속성 컨텍스트의 내용이 모두 지워져서 그 다음에 em에서 조회하는 경우, 데이터베이스의 정보를 가져온다.
java
열기
/* UPDATE
Member m
set
m.age = 20 */ update
Member
set
age=20
resultCnt = 3
member1.getAge() = 0
member2.getAge() = 0
member3.getAge() = 0
올바른 벌크 연산
java
열기
int resultCnt = em.createQuery("UPDATE Member m set m.age = 20") // 그런데, DB에는 20으로 업데이트
.executeUpdate();
em.clear();
Member findMember = em.find(Member.class, member1.getId()); // 영속성 컨텍스트에 값이 없으니까 DB에서 가져온다.
System.out.println("findMember = " + findMember.getAge());