2024.11.01 업데이트
페치 조인은 JPQL에서 엄청 엄청 중요한 내용 중 하나라는 생각이 든다. 왜 그러냐면 이 페치 조인이 연관된 엔티티를 SQL 한 번에 다 가져올 수 있는 방법인데 이게 왜 중요하냐? 이전에 N+1 문제에 대해 얘기하면서 이 N+1 문제는 즉시로딩뿐 아니라 지연로딩에서도 발생한다고 했다. 왜냐? 팀과 멤버를 생각해보면, 멤버를 전체 다 가져왔을 때 팀을 지연로딩으로 설정해 놓으면 최초 멤버를 가져올때는 팀을 쿼리해서 가져오지 않는다. 지연로딩이니까. 근데 이렇게 가져온 결과로 멤버에 접근해서 getTeam().getName() 이런 메서드를 호출하는 순간 팀을 가져오기 위한 프록시 초기화를 실행하고 이때 쿼리가 나간다. 즉, 지연로딩도 이렇게 N+1 문제가 발생한다는 이야기다. 그 문제를 해결할 수 있는 방법이 최초 한번의 SQL 쿼리로 멤버를 가져올 때 팀도 한번에 다 조인해서 가져오는 페치 조인을 사용하는 것이다.
사용법도 너무 간단하다. 아래처럼 JOIN FETCH로 조인할 연관 엔티티만 호출하면 된다.
SELECT m FROM Member m JOIN FETCH m.team
이 JPQL은 다음과 같은 SQL로 변환되어 나간다.
SELECT m.*, t.* FROM Member m INNER JOIN TEAM t ON m.team_id = t.id
m.*, t.*은 멤버와 팀의 모든 필드를 축약해서 작성한 것 뿐이고 그냥 내부 조인이 발생한다. 내부 조인이 발생하면 멤버와 팀의 데이터를 다 가져오기 때문에 한 번에 모든 데이터를 가져올 수 있는데 이게 사실 즉시 로딩이다. 그러나 즉시 로딩은 사용하면 안된다고 했었는데 즉시 로딩은 내가 멤버를 조회할 때 팀을 같이 조회할 필요가 없음에도 조인으로 가져오기 때문이었다. 그래서 지연 로딩이 기본으로 적용되어야 한다고 했는데 지연 로딩일 때 멤버를 조회하면 팀을 같이 조회해야 하는 경우 이 한 번의 쿼리로 모든 걸 다 해결할 수 있게 하는 것.
실제 사용 코드를 예시로 보자.
FETCH JOIN을 사용하지 않았을 때
package cwchoiit;
import cwchoiit.jpql.Member;
import cwchoiit.jpql.Team;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.util.List;
public class Main {
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("hellojpa");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
try {
for (int i = 0; i < 3; i++) {
Team team = new Team();
team.setName("Team" + i);
entityManager.persist(team);
Member member = new Member();
member.setName("member" + i);
member.setTeam(team);
entityManager.persist(member);
}
entityManager.flush();
entityManager.clear();
List<Member> findMembers = entityManager
.createQuery("SELECT m FROM Member m", Member.class)
.getResultList();
for (Member findMember : findMembers) {
System.out.println("findMember.getName() = " + findMember.getName());
System.out.println("findMember.getTeam().getName() = " + findMember.getTeam().getName());
}
entityManager.getTransaction().commit();
} catch (Exception e) {
entityManager.getTransaction().rollback();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
}
- 멤버와 그 멤버가 속할 팀을 루프안에서 만든다. 3명의 멤버가 각각 다른 팀에 속해있다. 멤버를 영속 컨텍스트에 영속시킨 후 루프가 끝난 후 영속 컨텍스트를 플러시 한다. 그 후에 모든 멤버를 가져와 각 멤버의 정보를 출력한다.
- 이때 발생되는 결과 SQL쿼리를 확인해 보자.
Hibernate:
/* SELECT
m
FROM
Member m */ select
member0_.id as id1_0_,
member0_.name as name2_0_,
member0_.TEAM_ID as team_id3_0_
from
Member member0_
findMember.getName() = member0
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
findMember.getTeam().getName() = Team0
findMember.getName() = member1
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
findMember.getTeam().getName() = Team1
findMember.getName() = member2
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
findMember.getTeam().getName() = Team2
나가는 SQL 쿼리에 집중해 보자. 최초에 멤버를 전부 조회하는 쿼리가 한 번 나간다. 합리적이다. 그런데 멤버의 팀을 찍으려고 할 때마다 팀을 조회하는 쿼리가 매번 나간다. 왜냐하면 멤버를 가져왔을 때 멤버가 가지고 있는 팀은 엔티티가 아닌 프록시이기 때문에 실제 사용하는 시점에 프록시에 엔티티 값을 집어넣어 줘야 하므로. 보면 팀 3개를 출력하기 위해 3번의 SQL문이 나간 게 보이는가? 이게 N+1 문제다. 지연로딩이든 즉시로딩이든 어떻게 사용하느냐에 따라 N+1 문제는 발생할 수 있게 된다.
그럼 지연로딩이었던 것을 즉시로딩으로 바꿀까? 절대 안 된다. 이 경우는 멤버를 조회할 때 팀도 조회하는 비즈니스 로직이기 때문에 이렇게 팀을 모두 조회하지만 그렇지 않은 로직을 타는 경우에는 불 필요한 조인이 발생하면 안된다. 이걸 해결해 주는 것이 페치조인이다.
FETCH JOIN을 사용할 때
package cwchoiit;
import cwchoiit.jpql.Member;
import cwchoiit.jpql.Team;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.util.List;
public class Main {
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("hellojpa");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
try {
for (int i = 0; i < 3; i++) {
Team team = new Team();
team.setName("Team" + i);
entityManager.persist(team);
Member member = new Member();
member.setName("member" + i);
member.setTeam(team);
entityManager.persist(member);
}
entityManager.flush();
entityManager.clear();
List<Member> findMembers = entityManager
.createQuery("SELECT m FROM Member m JOIN FETCH m.team", Member.class)
.getResultList();
for (Member findMember : findMembers) {
System.out.println("findMember.getName() = " + findMember.getName());
System.out.println("findMember.getTeam().getName() = " + findMember.getTeam().getName());
}
entityManager.getTransaction().commit();
} catch (Exception e) {
entityManager.getTransaction().rollback();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
}
- 코드에서 변경된 부분은 딱 하나. 쿼리문에 JOIN FETCH가 추가된 것뿐이다. 그러나 결과는 엄청난 차이가 있다. 결과문을 보자.
Hibernate:
/* SELECT
m
FROM
Member m
JOIN
FETCH m.team t */ select
member0_.id as id1_0_0_,
team1_.id as id1_3_1_,
member0_.name as name2_0_0_,
member0_.TEAM_ID as team_id3_0_0_,
team1_.name as name2_3_1_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
findMember.getName() = member0
findMember.getTeam().getName() = Team0
findMember.getName() = member1
findMember.getTeam().getName() = Team1
findMember.getName() = member2
findMember.getTeam().getName() = Team2
- SQL문 딱 하나에 모든 데이터가 들어오고 한 번에 모두 조회가 가능해졌다. 위에서 고작 멤버 3명을 조회하기 위해 쿼리 4번이 나간 반면, 지금은 단 한 번의 쿼리로 모든 걸 만족시켰다. 이게 페치 조인이다.
- 엔티티 타입을 페치 조인해봤으니 컬렉션 값 타입도 페치 조인해보자. 살짝 다르고 알아야 할 부분이 있다.
컬렉션 페치 조인 (일대다 관계, 컬렉션 페치 조인)
당연히 ManyToOne에서 페치 조인(멤버에서 팀을 가져올 때)이 가능하면 OneToMany에서도 페치 조인(팀에서 멤버들을 가져올 때)이 가능할건데 이 일대다에서 조인을 했을 때 조심할 부분이 있다. 우선 페치 조인으로 데이터를 가져와보자. 이건 페치 조인을 떠나서 그냥 일대다 입장에서 조인을 할 때 항상 조심할 부분이다.
package cwchoiit;
import cwchoiit.jpql.Member;
import cwchoiit.jpql.Team;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.util.List;
public class Main {
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("hellojpa");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
try {
Team team = new Team();
team.setName("TEAM1");
entityManager.persist(team);
Team team2 = new Team();
team2.setName("TEAM2");
entityManager.persist(team2);
Member member1 = new Member();
member1.setName("MEMBER1");
member1.setTeam(team);
Member member2 = new Member();
member2.setName("MEMBER2");
member2.setTeam(team);
Member member3 = new Member();
member3.setName("MEMBER3");
member3.setTeam(team2);
entityManager.persist(member1);
entityManager.persist(member2);
entityManager.persist(member3);
entityManager.flush();
entityManager.clear();
List<Team> findTeams = entityManager
.createQuery("SELECT t FROM Team t JOIN FETCH t.members", Team.class)
.getResultList();
for (Team findTeam : findTeams) {
System.out.println(findTeam.getName() + "|members.size(): " + findTeam.getMembers().size());
}
entityManager.getTransaction().commit();
} catch (Exception e) {
entityManager.getTransaction().rollback();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
}
- TEAM1, TEAM2 두 개의 팀이 있고, MEMBER1, MEMBER2, MEMBER3 세 명의 멤버가 있다.
- TEAM1에는 MEMBER1, MEMBER2가 포함되어 있다.
- TEAM2에는 MEMBER3이 포함되어 있다.
- 이 상태에서 팀을 뽑을건데 팀에 속한 Members를 페치 조인으로 가져와보자.
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_.name as name2_0_1_,
members1_.TEAM_ID as team_id3_0_1_,
members1_.TEAM_ID as team_id3_0_0__,
members1_.id as id1_0_0__
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
TEAM1|members.size(): 2
TEAM1|members.size(): 2
TEAM2|members.size(): 1
- 역시, 단 한번의 쿼리로 모든걸 해결했지만 이상한 점이 있다. TEAM1 데이터가 두번 찍힌다는 점이다. 즉, 가져온 findTeams에 3개의 레코드가 있다는 의미다. 왜 그럴까? 일대다 입장에서의 조인이기 때문이다.
아래 그림을 보면 이해가 된다.
팀을 기준으로 멤버와 조인하게 되면 팀A에 소속된 멤버가 두명이니까 두개의 레코드를 가진다. 이렇기에 팀A가 위처럼 두번 출력되는 것이다. 이렇게 일대다 조인은 데이터가 불어나게 되는데 이를 해결하기 위해서는 JPA에서는 해줄 수 있는게 없다. JPA입장에서는 이 데이터가 어떻게 만들어질지 전혀 알 지 못하는 상태이기 때문에.
그래서 DB로부터 조인한 쿼리로 받은 데이터는 이렇게 두 개가 들어오고 JPA는 그것을 객체로 표현해주기 때문에 다음과 같은 그림이 된다.
그래서 우선은 받은 객체가 두개니까 메모리에 주소값이 두개가 할당된다. 그러나, 그 두개의 객체는 같은 객체이므로 바라보는 주소는 같다. 영속 컨텍스트에는 같은 객체라면 하나만을 사용하면 되므로 영속 컨텍스트에는 하나의 객체만 있지만 조회한 findTeams 컬렉션에는 두개가 담겨있는 것. 그럼 중복을 제거하고 싶을 땐 어떻게 할까? DISTINCT를 사용하면 된다.
허나, SQL의 DISTINCT는 위 문제를 해결해주지 않는다. SQL에서 DISTINCT는 중복된 결과(완전히 똑같은)를 제거하는 명령이기 때문에 두 레코드를 중복으로 바라보지 않는다. 중복이 아니잖아. 레코드 기준으로 회원아이디도 회원이름도 다르니까.
그러나, JPQL의 DISTINCT는 2가지의 기능을 제공해준다.
- SQL에 DISTINCT를 추가해주는 작업
- 애플리케이션에 올라온 객체에서 엔티티 중복(주소가 같은 Team 객체)을 제거해준다. 이 엔티티 중복을 제거해줄 때 위 상황 같은 중복이 제거가 된다.
근데 그럼 중복 제거가 되면 문제가 없나요? SQL에서 DISTINCT는 완전히 똑같아야 제거를 해주는데 완전히 똑같지는 않잖아요?
▶ 맞다. 그런데 우리는 지금 객체지향 세계에 있다. 우리는 하나의 Team에 속한 모든 Member들을 가져오는 리스트를 Team 객체가 가지고 있기 때문에 어차피 그 안에 회원 2명이 담겨 오기 때문에 팀에 대한 중복 제거가 되도 아무런 문제가 없다.
그래서 결론은 DISTINCT 예약어를 추가해보자.
List<Team> teams = em.createQuery("SELECT DISTINCT t FROM Team t JOIN FETCH t.members", Team.class)
.getResultList();
실행 결과
Hibernate:
/* 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_.name as name2_0_1_,
members1_.TEAM_ID as team_id3_0_1_,
members1_.TEAM_ID as team_id3_0_0__,
members1_.id as id1_0_0__
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
TEAM1|members.size(): 2
TEAM2|members.size(): 1
일반 조인과 페치 조인의 차이점
일반 조인과 페치 조인은 어떤점이 다를까? 일반 조인은 조인을 하는건 맞다. 근데 그 조인 대상의 데이터를 메모리에 즉각적으로 퍼올리지는 않는다. 코드로 보는게 제일 빠르니 코드로 봐보자.
package cwchoiit;
import cwchoiit.jpql.Member;
import cwchoiit.jpql.Team;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.util.List;
public class Main {
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("hellojpa");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
try {
Team team = new Team();
team.setName("TEAM1");
entityManager.persist(team);
Team team2 = new Team();
team2.setName("TEAM2");
entityManager.persist(team2);
Member member1 = new Member();
member1.setName("MEMBER1");
member1.setTeam(team);
Member member2 = new Member();
member2.setName("MEMBER2");
member2.setTeam(team);
Member member3 = new Member();
member3.setName("MEMBER3");
member3.setTeam(team2);
entityManager.persist(member1);
entityManager.persist(member2);
entityManager.persist(member3);
entityManager.flush();
entityManager.clear();
List<Team> findTeams = entityManager
.createQuery("SELECT DISTINCT t FROM Team t JOIN t.members", Team.class)
.getResultList();
for (Team findTeam : findTeams) {
System.out.println(findTeam.getName() + "|members.size(): " + findTeam.getMembers().size());
}
entityManager.getTransaction().commit();
} catch (Exception e) {
entityManager.getTransaction().rollback();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
}
- 딱 아래 코드 부분에 집중해보자. 페치 조인이 아닌 일반 조인이다.
List<Team> findTeams = entityManager
.createQuery("SELECT DISTINCT t FROM Team t JOIN t.members", Team.class)
.getResultList();
실행 결과
Hibernate:
/* SELECT
DISTINCT t
FROM
Team t
JOIN
t.members */ select
distinct team0_.id as id1_3_,
team0_.name as name2_3_
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
Hibernate:
select
members0_.TEAM_ID as team_id3_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.name as name2_0_1_,
members0_.TEAM_ID as team_id3_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
TEAM1|members.size(): 2
Hibernate:
select
members0_.TEAM_ID as team_id3_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.name as name2_0_1_,
members0_.TEAM_ID as team_id3_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
TEAM2|members.size(): 1
- 최초에 SQL 쿼리를 보면, 멤버를 조인을 하는건 맞다. 그러나, SELECT절엔 팀 관련 데이터밖에 없다. 그렇기 때문에 내가 가져온 팀들에서 멤버를 조회하려고 할 때 다시 멤버관련 SQL문이 나가게 된다.
- 즉, 일반 조인은 일반 조인 과정에서 관련 엔티티 데이터를 퍼올리지 않는다. 프록시와 비슷한 상태인거지. 그래서 결국 멤버까지 가져오려면 다시 쿼리를 날리게 된다. 이런 차이가 있다.
페치 조인 한계
- 페치 조인 대상에는 별칭을 줄 수 없다.(하이버네이트는 가능하나 가급적 사용하지 않아야 한다).
List<Team> findTeams = entityManager
.createQuery("SELECT DISTINCT t FROM Team t JOIN FETCH t.members m", Team.class)
.getResultList();
저 코드에서 보면 JOIN FETCH 이후에 `t.members m` 이렇게 `m`으로 별칭을 주고 있는데 이런식으로 사용할 수 없다는 얘기다. 물론 하이버네이트는 가능하게 해준다. 그러나, 진짜로 페치 조인을 몇단계로 가져가야 할 경우 아닌 이상 절대 쓰지 않기로 하자. 그 외에 별칭을 가져가야 할 경우가 있다고 생각이 들면 그땐 다른 쿼리로 해결할 수 있는 경우가 무조건 있다고 생각해라.
- 둘 이상의 컬렉션은 페치 조인 할 수 없다.
예를 들어, 팀이 '멤버'라는 일대다 관계를 가지고 있고, 또 다른 뭐 '업무분야'라는 일대다 관계를 가지고 있을 때 이 두개를 같이 페치 조인하면 안된다. 그 이유는 위에서 봤지만 일대다만 해도 데이터가 부풀려 나오는데 이 경우는 일대다대다이다. 더 심한 데이터 부풀림 현상이 일어날 가능성이 농후하고 데이터 정합성에 문제가 발생할 수 있기 때문에 안된다.
- 컬렉션을 페치 조인(일대다 조인)하면 페이징 API(setFirstResult(), setMaxResults())를 사용할 수 없다.
일대다 페치 조인일 때 데이터 부풀림 현상이 일어나는것을 확인했다. 그런데 이것을 페이징한다? 데이터 정합성에 문제가 생길 가능성이 농후하다. 그냥 하면 안된다. 근데 문제는 하이버네이트는 경고 로그를 남기고 메모리에 데이터를 올린 후 그 메모리의 데이터로 페이징을 결국은 해준다는 것이다. 절대 절대 사용하면 안된다. 그냥 일대다 페치 조인을 할거면 다대일로 페이징을 하면 된다. 아래는 경고 로그를 남기고 페이징을 하는 예시이다.
List<Team> findTeams = entityManager
.createQuery("SELECT DISTINCT t FROM Team t JOIN FETCH t.members", Team.class)
.setFirstResult(0)
.setMaxResults(1)
.getResultList();
실행 결과
WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
전 죽어도 일대다로 페이징 하고 싶은데요?
▶ 그러면 방법이 있다. 가장 좋은 방법은 다대일로 페이징을 하면 되지만, 정말 때려죽어도 일대다로 페이징을 하고 싶다면, 다음과 같이 해보자.
List<Team> findTeams = entityManager
.createQuery("SELECT t FROM Team t", Team.class)
.setFirstResult(0)
.setMaxResults(2)
.getResultList();
for (Team findTeam : findTeams) {
System.out.println(findTeam.getName() + "|members.size(): " + findTeam.getMembers().size());
for (Member member : findTeam.getMembers()) {
System.out.println("member.getName() = " + member.getName());
}
}
- 우선은 페치 조인을 하지 말고 팀을 가져오는데 여기서 페이징을 원하는 만큼 한다. (위 코드에서는 0부터 2까지)
- 그 가져온 팀에서 멤버들을 가지고 와서 뿌려주면 된다. (위 코드에서 findTeam.getMembers()로 루프를 돌리는 부분)
- 그런데, 이렇게 되면 팀에 있는 멤버를 가져올때마다 지연로딩에 대한 초기화가 일어나기 때문에 쿼리가 계속 나간다 즉, N+1 문제가 발생한다. 이때 BatchSize를 설정해서 한번에 여러 멤버를 가져올 수가 있다! 아래 글로벌 설정 코드를 보자.
persistence.xml
...
<property name="hibernate.default_batch_fetch_size" value="100"/>
...
- 이렇게 batch_fetch_size를 100 정도로 설정을 하면, 팀에 있는 멤버들을 가져올 때 최대 100개까지 한번에 조회를 한다.
실행 결과
Hibernate:
/* SELECT
t
FROM
Team t */ select
team0_.id as id1_3_,
team0_.name as name2_3_
from
Team team0_ limit ?
Hibernate:
/* load one-to-many cwchoiit.jpql.Team.members */ select
members0_.TEAM_ID as team_id3_0_1_,
members0_.id as id1_0_1_,
members0_.id as id1_0_0_,
members0_.name as name2_0_0_,
members0_.TEAM_ID as team_id3_0_0_
from
Member members0_
where
members0_.TEAM_ID in (
?, ?
)
TEAM1|members.size(): 2
member.getName() = MEMBER1
member.getName() = MEMBER2
TEAM2|members.size(): 1
member.getName() = MEMBER3
위 쿼리를 보면 지연로딩으로 인한 프록시 초기화를 하기 위해 두번째 쿼리를 날리는데 그때 WHERE절을 잘보면 IN을 사용해서 TEAM_ID가 그 중 하나에 속했다면 멤버들을 다 가져오는 쿼리를 날리고 있다. 지금은 팀이 전체 2개밖에 없으니까 딱 IN 안에 2개만 들어갔지만, 만약 200개 300개가 넘으면 내가 설정한 BatchSize인 100개를 IN에 넣어 한번에 좀 여러개의 멤버를 가져올 수 있다. 이렇게 최적화를 하면서 일대다 페이징을 처리할 수도 있다.
페치조인 한계 마무리
- 모든 것을 페치 조인으로 해결할 수는 없다. (그러나, 거의 95%는 페치 조인으로 다 해결이 된다고 본다)
- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다. 그러니까 멤버와 팀이 있으면 각각이 가진 필드들이나 참조를 그대로 반환하고 사용하는 경우를 말한다. 그렇지 않은 경우가 아래 경우다.
- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면 페치 조인보다는 일반 조인을 사용해서 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
다형성 쿼리
그렇게 중요한 부분은 아닌데 알고 가면 좋으니까 추가적으로 작성한다. 이전 포스팅에서 상속 관계 매핑에서 배운 부모-자식 관계에 대한 엔티티가 있을 때에 대한 이야기이다.
이렇게 생겨먹은 엔티티 구조가 있을 때, 조회 대상을 특정 자식으로 한정하는 쿼리에 대해 알아보자.
예를 들어, "Item 중에 Book, Movie를 조회해라" 라는 질의가 있으면 이렇게 하면 된다.
// JPQL
SELECT i
FROM Item i
WHER TYPE(i) IN (Book, Movie)
// SQL
SELECT i
FROM Item i
WHERE i.DTYPE IN ('Book', 'Movie')
또는 캐스팅과 같은 방법도 있다. TREAT라는 키워드인데, 예를 들어 부모인 Item과 자식 Book이 있을 때, 이런식으로 작성할 수 있다.
// JPQL
SELECT i
FROM Item i
WHERE TREAT(i as Book).author = 'kim'
// SQL
SELECT i.*
FROM Item i
WHERE i.DTYPE = 'Book' AND i.author = 'kim'
엔티티 직접 사용
무슨 말이냐면, 바로 JPQL을 보자.
엔티티 직접 사용 - 기본키 값
// JPQL
SELECT COUNT(m.id) FROM Member m // 엔티티의 아이디를 사용
SELECT COUNT(m) FROM Member m // 엔티티를 직접 사용
- 이런 식으로 COUNT를 사용할 때 엔티티의 기본키를 사용하는게 아니라 엔티티 자체를 가지고 사용하는 걸 말하는데 이러면 어떻게 될까?
▶ SQL로 번역될 때 해당 엔티티의 기본키 값을 사용한다.
// 번역된 SQL
SELECT COUNT(m.id) as cnt FROM Member m
엔티티 직접 사용 - 파라미터 값
위와 같은 경우 말고, 파라미터로 전달할 때도 엔티티를 넘길 수 있는데 이것 역시 식별자로 변환된다.
엔티티를 파라미터로 전달
String jpql = "select m from Member m where m = :member";
List result = em.createQuery(jpql).setParameter("member", member).getResultList();
식별자를 전달
String jpql = "select m from Member m where m.id = :memberId";
List result = em.createQuery(jpql).setParameter("memberId", memberId).getResultList();
실행된 SQL (둘 다 마찬가지로)
SELECT m.*
FROM Member m
WHERE m.id=?
엔티티 직접 사용 - 외래키 값
이번엔 외래키 자리에 넣을 값도 엔티티 자체를 넘길수도 있는데 이것 역시 마찬가지로 식별자로 변환된다.
외래키 자리에 엔티티를 직접 전달
String jpql = "select m from Member m where m.team = :team";
List result = em.createQuery(jpql).setParameter("team", team).getResultList();
외래키 자리에 식별자를 전달
String jpql = "select m from Member m where m.team.id = :teamId";
List result = em.createQuery(jpql).setParameter("teamId", teamId).getResultList();
실행된 (둘 다 마찬가지로)
SELECT m.*
FROM Member m
WHERE m.team_id=?
'JPA(Java Persistence API)' 카테고리의 다른 글
[JPA] Part 18. 벌크 연산 (0) | 2023.10.30 |
---|---|
[JPA] Part 17. Named Query (2) | 2023.10.30 |
[JPA] Part 15. JPQL (0) | 2023.10.28 |
[JPA] Part 14. 값 타입 컬렉션 (0) | 2023.10.25 |
[JPA] Part 13. 임베디드 값 타입 (0) | 2023.10.23 |