페치 조인(fetch join)
- SQL의 조인 종류가 아닌JPQL의 특수한 기능
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
- join fetch 명령어 사용
- 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
예시)
member1,3을 teamA에 member2를 teamB에 넣었다고 가정하자.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Team team2 = new Team();
team2.setName("teamB");
em.persist(team2);
Member member = new Member();
member.setName("member1");
member.setAge(10);
member.setTeam(team);
em.persist(member);
Member member2 = new Member();
member2.setName("member2");
member2.setAge(10);
member2.setTeam(team2);
em.persist(member2);
Member member3 = new Member();
member3.setName("member3");
member3.setAge(10);
member3.setTeam(team);
em.persist(member3);
일반 조인하여 Member를 조회했을 때(다대일 관계)결과는 아래와 같이 나오지만 쿼리문을 본다면 team 객체에 대한 정보를 조회할 때마다 쿼리를 날리는 것 을 알수 있다. 하지만 fetchJoin을 사용하면 Member 객체를 조회 할 때 한번에 관련된 엔티티를 모두 조회해온다.
List<Member> fetchJoinList = em.createQuery("SELECT m FROM Member m JOIN fetch m.team", Member.class).getResultList();
List<Member> joinQuery = em.createQuery("SELECT m FROM Member m, Team t where m.name = t.name", Member.class).getResultList();
for (Member member1 : fetchJoinList) {
System.out.println("member1.getName() , member1.getTeam().getName() = " + member1.getName() + "," + member1.getTeam().getName());
}
for (Member member2 : joinQuery) {
System.out.println("member2.getName() , member2.getTeam().getName() = " + member2.getName() + "," + member1.getTeam().getName());
}
//결과값
member1.getName() , member1.getTeam().getName() = member1,teamA
member1.getName() , member1.getTeam().getName() = member2,teamB
member1.getName() , member1.getTeam().getName() = member3,teamA
member2.getName() , member2.getTeam().getName() = member1,teamA
member2.getName() , member2.getTeam().getName() = member2,teamB
member2.getName() , member2.getTeam().getName() = member3,teamA
실제 쿼리 문
위와 같이 테이블을 Join 하여 데이터를 조회 할 때 1+N현상 (1번 쿼리를 날릴 때 연관된 엔티티에 따라서 추가적으로 SELECT JOIN 쿼리나 나가는 현상) 을 방지할 수 있다.
일대다 연관관계시 테이블을 JOIN하여 SELECT해오면 데이터가 늘어나게된다.
하지만 반대로 Team 객체를 조회했을때(일대다 관계)는 Team 엔티티는 2개 밖에 없음에도 불구하고 join이 되면서 3개로 데이터가 늘어나게 된다.
예시)
일대다 관계에서 Team 테이블의 데이터를 가져올 때 Join된 연관 테이블로 인해 데이터 수가 늘어나게 된다.
JPQL로 조회하였을때
List<Team> fetchJoinList2 = em.createQuery("SELECT t FROM Team t JOIN fetch t.members", Team.class).getResultList();
for (Team teamResult1 : fetchJoinList2) {
System.out.println("team|member = " + teamResult1.getName() + "," + teamResult1.getMembers());
for (Member team1Member : teamResult1.getMembers()) {
System.out.println("Member = " + team1Member.getName());
}
}
아래와 같은 결과값을 얻게 된다.
이는 조인시 테이블이 2개에서 3개로 증가하였고 이로 인해 Team의 PK값이 증가 (teamA에 속한 맴버가 2명이므로)하여 조회 시 같은 Team엔티티를 2번 조회한 결과가 나온것이다. 이 때문에 Team의 객체 수에 맞는 결과값 (결과 값이 teamA, teamB 2개만 나오기 원한다면) DISTINCT(중복 제거)가 추가적으로 필요하다.
페치조인 DISTINCT
- SQL의 DISTINCT는 중복된 결과를 제거하는 명령 (모든 컬럼이 완전 동일한 데이터를 제거함, 따라서 객체 입장에서의 중복 제거는 JPQL에서 해줌)
- JPQL의 DISTINCT 2가지 기능 제공
- SQL에 DISTINCT를 추가
- 애플리케이션에서 엔티티 중복 제거
DISTICT 적용한 JPQL
List<Team> fetchJoinList2 = em.createQuery("SELECT DISTINCT t FROM Team t JOIN fetch t.members", Team.class).getResultList();
for (Team teamResult1 : fetchJoinList2) {
System.out.println("team|member = " + teamResult1.getName() + "," + teamResult1.getMembers());
for (Member team1Member : teamResult1.getMembers()) {
System.out.println("Member = " + team1Member.getName());
}
}
결과값
페치조인과 일반조인의 차이
일반 조인
실행시 연관된 엔티티를 함께 조회하지 않음
• [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'
• JPQL은 결과를 반환할 때 연관관계 고려하지 않음
• 단지 SELECT 절에 지정한 엔티티만 조회함 이외 절은 조회하지 않는다.
• 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회하지 않는다. (Member 테이블의 TEAM_ID만 가져와서 JOIN함)
페치 조인
페치 조인은 연관된 엔티티를 함께 조회함
• [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
• 페치 조인을 사용할 때만 연관된 엔티티도 SELECT절에서 함께 조회(즉시 로딩)
• 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념
페치조인의 특징
- 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함 (@OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략)
- 실무에서 글로벌 로딩 전략은 모두 지연 로딩
- 최적화가 필요한 곳은 페치 조인 적용
페치조인의 한계
- 페치 조인 대상에는 별칭(엘리어스)을 줄 수 없다. 하이버네이트는 가능, 가급적 사용X
- 둘 이상의 컬렉션은 페치 조인 할 수 없다. (데이터 증가 이슈)
- 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.(데이터가 얼마나 증가될 지 모르는 상황에서 범위를 정하면 안된다.)
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
참고사항
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적
엔티티 직접 사용
JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용
• [JPQL]
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용
• [SQL]
select count(m.id) as cnt from Member m
엔티티를 직접 넣었을 때 JPQL에서 해당 엔티티를 객체의 Id값으로 변환해서 SQL화 함
Named쿼리 에노테이션
엔티티에 @NamedQuery 에노테이션으로 메서드명과 실행 할 쿼리문을 지정하면 JPQL에서 해당 메서드명을 호출하여 쿼리를 실행할 수 있다.
• [Entity]
@Entity
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
• [JPQL]
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username","회원1")
.getResultList();
- 미리 정의해서 이름을 부여해두고 사용하는 JPQL
- 작성된대로 사용되는 정적 쿼리 역할을 한다.
- 어노테이션으로 사용 가능하며 XML에 정의할 수도 있다.
- 애플리케이션 로딩 시점에 초기화 후 재사용이 가능하다.
- 애플리케이션 로딩 시점에 쿼리를 검증해준다.
참고사항
Repository를 생성할 때 매우 자주 활용하는 SpringDataJpa가 인터페이스 형식으로 해당 방식으로 구현해 놓았다.
벌크연산
만약 데이터 값을 전체적으로 변경하고자 할 때 JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행하는 비효율적인 쿼리문이 나가게 될 것이다. 이때 벌크연산을 사용 할 수 있다.
벌크연산 특징
- 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
- executeUpdate()의 결과는 영향받은 엔티티 수 반환
- UPDATE, DELETE 지원
- INSERT(insert into .. select, 하이버네이트 지원)
em.createQuery("update Member m " +
"set m.grade = 'vip' " +
"where m.orderAmount < :orderAmount")
.setParameter("stockAmount", 1000000)
.executeUpdate();
//오더 총액이 100만 이상이면 VIP로 등급 설정한다.
벌크 연산 주의 :
- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 날림
- 한 트렌잭션 안에서 변경된 값을 받고 얻고 싶을 때 벌크 연산을 가장 먼저 실행하거나 벌크 연산 수행 후 영속성 컨텍스트 바로 초기화 해줘야함
'JPA' 카테고리의 다른 글
JPA , QueryDsl 수정 중 에러 해결 (0) | 2024.03.17 |
---|---|
영속성 컨텍스트 (0) | 2024.01.28 |
JPQL 문법 1 (1) | 2022.12.22 |
JPA 쿼리 언어 종류 (1) | 2022.12.22 |
값 타입 정리 (0) | 2022.12.21 |