경로표현식 구분
점을 찍어 객체 그래프를 탐색하는 것
상태필드
- 단순히 값을 저장하기 위한 필드
- 경로 탐색의 끝. 더이상 탐색 X
연관필드 : 연관관계를 위한 필드
단일 값 연관 경로
- 묵시적 내부 조인 발생 (묵시적 내부조인은 사용하지 않는 것이 좋다)
- 탐색 더 가능
컬렉션 값 연관 경로 (1 대 다 관계)
- 묵시적 내부 조인 발생
- 탐색 X (from절에서 명시적 조인을 사용해서 별칭을 얻어서 사용하면 탐색이 가능하다)
결론 : 실무에서는 묵시적조인을 사용하지 않고 명시적 조인만 사용한다.
fetch join (페치조인)
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
- join fetch 로 사용
사용예제
회원 1 : teamA
회원 2 : teamA
회원 3 : teamB
상황일때
페치조인 사용X
String query = "select m from Member m";
List<Member> resultList = em.createQuery(query, Member.class)
.getResultList();
for (Member member : resultList) {
System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
}
실행결과
Member와 Team은 일대다 관계이고 지연로딩설정을 해놓았기 때문에 member.getTeam() 시점에서 team 쿼리가 날아간다.
* SQL 3개
Member List : SQL
teamA : SQL
teamA : 영속성 컨텍스트
teamB : SQL
두번째 teamA는 이미 SQL가 날아가고 영속성 컨텍스트에 올라갔기 때문에 SQL이 나가지 않는다.
패치조인 사용O
String query = "select m from Member m join fetch m.team";
List<Member> resultList = em.createQuery(query, Member.class)
.getResultList();
for (Member member : resultList) {
System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
}
페치조인으로 회원과 팀을 함께 조회해서 지연로딩없이 미리 가져온다.
실행결과
컬렉션 페치 조인 (일 대 다) 도 가능하다.
그런데 team과 member를 조인할 때 팀과 연결된 멤버 수 대로 테이블이 구성된다.
- 팀A는 처음에 쿼리가 나가고 영속성 컨텍스트에 등록된다. 그래서 2번째 팀A는 영속성 컨텍스트에서 가져와 사용하기 때문에 1개로 표현한다.
- 회원은 컬렉션 fetch join (일대다) 일때 DB입장에서는 회원의 개수만큼 가져와서 2개가 된다.
예제코드
String query = "select t from Team t join fetch t.members";
List<Team> resultList = em.createQuery(query, Team.class)
.getResultList();
for (Team team : resultList) {
System.out.println("team = " + team.getName() + " | " + team.getMembers().size());
for (Member member : team.getMembers()) {
System.out.println("-> member = " + member);
}
}
결과
페치조인, DISTINCT
- sql의 distinct : 완전히 일치하면 중복 제거
- jpql의 distinct : 완전히 일치하면 중복제거, 같은 식별자를 가진 엔티티 중복 제거
String query = "select distinct t from Team t join fetch t.members";
2번째 team이 영속성 컨텍스트에 등록된 엔티티를 가져오기 때문에 같은 엔티티이고 중복이 제거된다.
페치조인 vs 일반조인
페치조인
String query = "select t from Team t join fetch t.members";
fetch join 이므로 t를 가져올때 member까지 미리 가저온다.
일반조인
String query = "select t from Team t join t.members m";
select절에 t만 있으므로 m은 당연히 가져오지 않는다. 그래서 나중에 team에서 getMembers를 할때 쿼리문이 날아간다.
페치 조인의 특징과 한계
- 페치 조인 대상에는 별칭을 줄 수 없다.
(별칭을 준다는 것은 where로 조건을 줄 수 있다는 것인데, jpa는 연관된 객체그래프를 전부 가져온다고 생각하고 다른 옵션(cascade 등)을 부여할 수 있기 때문에 애초에 별칭을 주지 않고 사용하자는 관례가 있다.) - 둘 이상의 컬렉션은 페치 조인 할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API(setFirstREsult, setMaxResults)를 사용할 수 없다.
(join fetch를 뺴고 페이징 API를 사용하고 컬렉션을 가지고있는 엔티티에는 batch size를 줘서 한번에 가져오는 엔티티 수를 정할 수 있다.)
정리
- 모든 글로벌 로딩 전략은 지연로딩으로 한다.
- 필요한 경우 페치조인으로 가져온다.
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 할 때
- 엔티티를 페치조인으로 조회해 온다. 애플리케이션에서 DTO로 바꿔서 화면에 반환한다.
- 처음 jpql을 짤때부터 new operation으로 스위칭해서 가져온다
다형성 쿼리
Item 중에서 Book, Movie만 가져오고 싶을 때
- JPQL
select i from Item i where type(i) IN (Book, Moive) - SQL
select i from item i where i.DTYPE IN ('B', 'M')
Treat
- JPQL
select i from Item i where treat(i as Book).author = 'kim' - SQL
select i from item i where i.DTYPE = 'B' and i.author = 'kim'
JPQL 엔티티 직접사용
- 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 둘다 이 SQL 실행
JPQL에서 엔티티를 직접사용하면 '기본키'를 사용한다.
+ DB에서 외래키로 되어있는 엔티티도 엔티티 그대로 넘기면 외래키를 넘긴것과 같다.
Named 쿼리 - 어노테이션
- 미리 정의해서 이름을 부여해두고 사용하는 JPQL
- 정적쿼리만 가능
- 어노테이션, XML에 정의
- 애플리케이션 로딩시점에 초기화 후 재사용
- 애플리케이션 로딩 시점에 쿼리를 검증
Spring Data JPA 에서 jpa가 사용한다.
벌크 연산
재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
너무 많은 update query가 실행된다.
JPQL은 벌크연산이 가능하다.
int resultCount = em.createQuery("update Member m set m.age = 20")
.executeUpdate();
System.out.println("resultCount = " + resultCount);
쿼리 한번으로 여러 테이블 로우 변경 가능
벌크 연산 주의
벌크연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다.
벌크연산 코드
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.setAge(0);
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("관리자2");
member2.setAge(0);
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("관리자3");
member3.setAge(0);
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
int resultCount = em.createQuery("update Member m set m.age = 20")
.executeUpdate();
System.out.println("resultCount = " + resultCount);
System.out.println("member1.getAge() = " + member1.getAge());
System.out.println("member2.getAge() = " + member2.getAge());
System.out.println("member3.getAge() = " + member3.getAge());
출력결과
영속성 컨텍스트를 무시하기 때문에 member의 나이를 20살로 업데이트해도 영속성 컨텍스트에서 age를 가져오면 0살로 되어있다.
그래서, 아래 두가지 방법중 하나를 선택해서 사용해야한다.
- 벌크 연산만 먼저 실행
- 벌크연산 수행 후 영속성 컨텍스트 초기화
대부분 2번을 적용해준다 ( 벌크연산 후 em.clear() )
'Web > JPA' 카테고리의 다른 글
EP2. 지연 로딩과 조회 성능 최적화 (0) | 2021.04.06 |
---|---|
EP1. 회원관리 API 만들기 (0) | 2021.04.05 |
EP12. JPQL (SQL식 JPQL변환) (0) | 2021.02.21 |
EP11. JPQL소개, 기본문법 (0) | 2021.02.19 |
CascadeType.REMOVE vs orphanRemoval = true (0) | 2021.02.13 |