경로 표현식
경로 표현식은 점을 찍어 객체 그래프를 탐색하는 것이다.
select m.username -> 상태 필드
from Member m
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'
- 상태 필드(state field) : 단순히 값을 저장하기 위한 필드, 경로 탐색의 끝이므로 더이상 탐색이 불가능하다.
JPQL: select m.username, m.age from Member m
SQL: select m.username, m.age from Member m
- 연관 필드(association field) : 연관관계를 위한 필드,
- 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티 , 단일 값 연관 경로는 묵시적 내부 조인이 발생한다 탐색이 가능하다. 튜닝이 어려우므로 최소한으로 사용해야한다.
JPQL: select o.member from Order o
SQL:
select m.*
from Orders o
inner join Member m on o.member_id = m.id
- 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션, 묵시적 내부 조인이 발생한다. 더 이상 탐색이 불가능하다. -> From 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능하다.
select m.username from Team t join t.members m
명시적 조인, 묵시적 조인
- 명시적 조인 : join 키워드 직접 사용한다.
- 묵시적 조인 : 경로 표현식에 의해 묵시적으로 SQL 조인 발생(내부 조인만 가능)
경로 탑색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인이다
- 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야한다.
- 경로 탐색은 주로 SELECT, WHERE절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM(JOIN)절에 영향을 준다.
실무 조언
- 가급적 묵시적 조인 대신에 명시적 조인 사용
- 조인은 SQL 튜닝에 중요 포인트이다.
- 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵다.
페치 조인(Fetch Join)
페치 조인은 SQL 조인 종류가아니다. JPQL에서 성능 최적화를 위해 제공하는 기능이다. 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능이다. join fetch 명령어를 사용한다.
- 페치 조인 = [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
엔티티 페치 조인
예를 들어 회원을 조회하면서 연관된 팀도 함께 조회하고 싶다.
[JPQL]
select m from Member m join fetch m.team
[SQL]
SELECT M.*, T.* FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID
SQL을 보면 회원 뿐만 아니라 팀도 함께 SELECT한다.
ManyToOne fetch Join 예제
위 그림에서 Member와 Team의 관계와 Inner Join으로 검색했을 때 Table이 어떻게 형성 되었는지 보여준다.
String query =
"select m from Member m";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
for (Member member : result) {
System.out.println("member.getUsername() = " + member.getUsername() + ", " + member.getTeam().getName());
}
이렇게 검색했을 경우 팀 하나당 select문이 하나씩 나온다. 최악의 경우 N + 1번 select하므로 효율이 엄청 않 좋아진다.
해결책으로 fetch join이 있다.
String query =
"select m from Member m join fetch m.team";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
//result에 내용이 들어갈 때 부터 프록시가 아닌 실제 DATA이다
for (Member member : result) {
System.out.println("member.getUsername() = " + member.getUsername() + ", " + member.getTeam().getName());
}
컬렉션 fetch Join 예제
일대다 관계, 컬렉션 페치를 조인한다.
팀당 Member가 몇명이는 JPQL문을 구현했다.
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() + "|" + team.getMembers().size());
}
//출력값
//team = 팀A|2
//team = 팀A|2
//team = 팀B|1
출력값이 팀A일 때, 2번 출력된다.
팀 A에는 2명이 회원이 있어 각각 select를 하므로 회원 1일 때 팀A|2, 회원 2일 때 팀A|2 2번 출력된다.
페치 조인과 DISTINCT
SQL의 DISTINCT는 중복된 결과를 제거하는 명령 이지만 JPQL의 DISTINCT 2가지 기능 제공한다.
- SQL에 DISTINCT를 추가
- 애플리케이션에서 엔티티 중복 제거
String query = "select distinct t from Team t join fetch t.members";
SQL에 DISTINCT를 추가했다.
하지만 왼쪽의 Table을 보면 첫번째 행과 두번째 행이 똑같지가 않다. 그러므로 DISTINCT는 실패한다.
그래서 JPQL은 DISTINCT가 추가로 애플리케이션에서 중복 제거를 시도한다.
같은 식별자를 가진 Team엔티티를 제거하게 된다.
페치 조인과 일반 조인의 차이
일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다.
- JPQL은 결과를 반환할 때 연관관계 고려하지 않는다.
- 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
- 일반 조인은 팀 엔티티만 조회하고, 회원 엔티티는 조회하지 않는다.
- 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)한다.
- 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념이다.
페치 조인의 특징과 한계
- 페치 조인 대상에는 별칭을 줄 수 없다. 하이버네이트는 가능하지만 가급적 사용하지 말자!
- 둘 이상의 컬렉션은 패치 조인 할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다.
- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징을 한다. -> 매우 위험하다.
컬렉션 페치 조인에도 페이징을 하기 위해 해결책은 batchSize를 사용하는 것이다.
컬렉션 페치 조인은 N + 1번 select 구문이 나간다. 그것을 해결하기 위해 batchSize를 1000이하의 정수로 두면 한번에 select를 할 때, 선택한 정수만큼 모두 가져온다. SQL의 where문에 ?가 팀 갯수만큼 추가된다.
@BatchSize(size = 100)
//GROBAL SETTING
//persistence.xml에 추가
<property name = "hibernate.default_batch_fetch_size" value="100" />
- 결국 패치조인은 연관된 엔티티들을 SQL 할 번으로 조회하기 위해 사용하는 것이다.
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선된다.
- 실무에서 글로벌 로딩 전략은 모두 지연 로딩으로 변경해야한다.
- 최적화가 필요한 곳 N+1이 일어나는 곳은 페치 조인을 적용한다.
- 패치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요 한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
다형성 쿼리
TYPE
조회 대상을 특정 자식으로 한정한다. 만약 ITEM 중에 Book, Movie를 조회해야 한다면
//JPQL
select i from Item i where type(i) in (Book, Movie)
//SQL
select i from i where i.DTYPE in (‘B’, ‘M’)
TREAT
- 자바의 타입 캐스팅과 유사한다.
- 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.
- FROM, WHERE, SELECT(하이버네이트 지원)에서 모두 사용 가능하다.
만약 부모인 ITEM과 자식 BOOK이 있다고 하자.
//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'
엔티티 직접 사용
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
엔티티를 파라미터로 전달이 가능하다.
String query =
"select m from Member m where m = :member";
Member findMember = em.createQuery(query, Member.class)
.setParameter("member", member1)
.getSingleResult();
System.out.println("findMember = " + findMember);
//출력값
//findMember = Member{id=3, username='회원1', age=0}
//식별자를 직접 전달도 가능하다.
String query =
"select m from Member m where m.id = :memberId";
Member findMember = em.createQuery(query, Member.class)
.setParameter("memberId", member1.getId())
.getSingleResult();
System.out.println("findMember = " + findMember);
둘다 출력값은 동일하다.
외래 키 값
Member에서 team은 외래 키로 지정 되어있다. 외래 키 값을 직접 사용한.
String query =
"select m from Member m where m.team = :team";
List<Member> team = em.createQuery(query, Member.class)
.setParameter("team", teamA)
.getResultList();
for (Member member : team) {
System.out.println("member = " + member);
}
Named 쿼리
정적 쿼리
- 미리 정의해서 이름을 부여해두고 사용하는 JPQL이다.
- 어노테이션 및 XML에 정의한다.
- 애플리케이션 로딩 시점에 초기화 후 재사용한다.
- 애플리케이션 로딩 시점에 쿼리를 검증한다. 오류를 잡아 줄 수 있다.
사용
//Member클래스에 추가
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"
)
//Main
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
for (Member member : resultList) {
System.out.println("member = " + member);
}
스프링데이터 JPA에서는 @Query에 쿼리를 지정하면서 이름없는 NAMED쿼리를 사용할 수 있다.
@Query("select u from User u where u.email(Address = ?1")
User findByEmailAddress(String emailAddress);
벌크 연산
재고가 10개 미만인 모든 상품의 가격을 10% 상승시켜야 한다고 하자
JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL을 실행 시켜야 한다.
재고가 10개 미만인 상품을 리스트로 조회를 하고 상품 엔티티의 가격을 10% 증가시킨다. 그 후 트랜잭션 커밋 시점에 변경감지가 동작한다. 변경된 데이터가 많으면 많을수록 많은 쿼리가 나가고 효율이 않좋아진다. 이것을 벌크 연산으로 해결 할 수가 있다.
벌크 연산 예제
- 쿼리 한 번으로 여러 테이블 로우를 변경 할 수있다.
- executeUpdate()의 결과는 영향받은 엔티티 수를 반환한다.
- Update, Delete 모두 지원한다.
- INSERT(insert into .. select, 하이버네이트 지원)
//Flush 자동 호출
int resultCount = em.createQuery("update Member m set m.age = 20")
.executeUpdate();
System.out.println("resultCount = " + resultCount);
벌크 연산 주의
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 바로 집어 넣는다. 벌크 연산을 할 때 지금 영속성 컨텍스트에 있는게 모두 flush가 된다. 위의 예제 처럼 벌크 연산 전에 나이가 20살이 아니면 애플리케이션은 벌크 연산전에 가지고 있던 나이가 그대로 남아 있는다. 하지만 DB는 20살로 나이가 변경된다. 이렇게 애플리케이션과 DB가 꼬일 수가 있다.
int resultCount = em.createQuery("update Member m set m.age = 20")
.executeUpdate();
em.clear();
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember.getAge() = " + findMember.getAge());
//em.clear()를 안하면 0이 출력한다.
해결책은 벌크 연산을 먼저 실행하거나 벌크 연산 수행 후 영속성 컨텍스트를 초기화한다.
강의 출처 : https://www.inflearn.com/course/ORM-JPA-Basic
'Spring JPA 공부 > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
JPA - JPQL 기본 문법과 기능 (0) | 2022.12.30 |
---|---|
JPA - 객체지향 쿼리 언어(JPQL) 소개 (0) | 2022.12.30 |
실전 예제 6 - 값 타입 매핑 (0) | 2022.12.29 |
JPA - 값 타입 (0) | 2022.12.29 |
실전 예제 5 - 연관관계 관계 (0) | 2022.12.28 |