2023. 02.07 18:20 복습 시작
이제 상대적으로(?) 저번 시간보다 좀 더 난이도 있는 JPQL 문법에 대해 알아보겠다.
JPQL 경로 표현식
JPQL에서 사용하는 경로 표현식을 알아보고 경로 표현식을 통한 묵시적 조인도 알아보자.
경로 표현식이라는 것은 쉽게 이야기해서 .(점)을 찍어 객체 그래프를 탐색하는 것이다.
select m.username -> 상태 필드
from Member m
join m.team t -> 단일 값 연관 피드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'
경로 표현식 용어를 정리해보자.
- 상태 필드(state field) : 단순히 값을 저장하기 위한 필드 (ex: m.username)
- 연관 필드(association field) : 연관관계를 위한 빌드
- 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티 (ex: m.team)
- 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션 (ex: m.orders)
경로 표현식 특징
- 상태 필드 : 경로 탐색의 끝, 탐색 X
- 단일 값 연관 경로 : 묵시적 내부 조인 발생, 탐색 O
- 컬렉션 값 연관 경로 : 묵시적 내부 조인 발생, 탐색 X.
- FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색이 가능하다.
상태 필드 경로 탐색
JPQL: select m.username, m.age from Member m
SQL: select m.username, m.age from Member m
단일 값 연관 경로 탐색
JPQL: select o.member from Order o
SQL: select m.* from Orders o inner join Member m on o.member_id = m.id
단일 값 연관 경로 탐색 JPQL을 보면 o.member를 통해 주문에서 회원으로 단일 값 연관 필드로 경로 탐색을 했다.
단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이것을 묵시적 조인이라고 한다.
참고로 묵시적 조인은 모두 내부 조인이다. 외부 조인은 명시적으로 JOIN 키워드를 사용해야 한다.
JPQL을 다루면서 많이 하는 실수 중 하나는 컬렉션 값에서 경로 탐색을 시도하는 것이다.
select t.members from Team t //성공
select t.members.username from Team t //실패
t.members처럼 컬렉션까지는 경로 탐색이 가능하다.
하지만 t.members.username처럼 컬렉션에서 경로 탐색을 시작하는 것은 허락하지 않는다.
만약 컬렉션에서 경로 탐색을 하고 싶으면 다음 코드처럼 조인을 사용해서 새로운 별칭을 획득해야 한다.
select m.username from Team t join t.members m
이제 m으로 컬렉션에 새로운 별칭을 얻었다. 이제 이 m을 가지고 경로 탐색을 할 수 있다.
경로 탐색을 사용한 묵시적 조인 시 주의사항이 있다.
- 항상 내부 조인이다.
- 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 한다.
- 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 준다.
정리
가급적 묵시적 조인 대신에 명시적 조인을 사용하자!
페치 조인
이번에는 정말 정말 중요한 페치 조인이다.
패치 조인은 SQL의 종류는 아니다. JPQL에서 성능 최적화를 위해 제공하는 기능이다.
연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능인데 join fetch 명령어로 사용할 수 있다.
사용법 : [Left [OUTER] | INNER ] JOIN FETCH 조인 경로
페치 조인을 사용해서 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회하는 JPQL을 확인해보자.
사용한 [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 한다. 아래 이미지로 쉽게 이해할 수 있다.
이번에는 컬렉션 페치 조인에 대해 알아보겠다. 일대다 관계의 예시다.
[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'
결과는 아래 이미지로 쉽게 이해할 수 있다.
보면 우리는 JPQL에서 select t로 팀만 선택했는데 예제 SQL을 보면 T.*, M.*로 팀과 연관된 회원도 함께 조회한 것을 확인할 수 있다.
그리고 그림 TEAM 테이블에서 '팀A'는 하나지만 MEMBER 테이블과 조인하면서 결과가 증가해서 조인 결과 테이블을 보면 같은 '팀A'가 2건 조회되었다.
따라서 컬렉션 페치 조인 결과 객체에서 teams 결과 예제를 보면 주소가 같은 '팀A'를 2건 가지게 된다.
참고 - 일대다 조인은 결과가 증가할 수 있지만, 일대일 다대일 조인은 결과가 증가하지 않는다.
이제 위처럼 '팀A'가 2건이 조회되는 경우를 막기 위한 방법을 배워보자. 바로 DISTINCT다.
SQL의 DISTINCT는 중복된 결과를 제거하는 명령이다.
JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론이고 애플리케이션에서 한 번 더 중복을 제거한다.
바로 직전 컬렉션 페치 조인은 팀A가 중복으로 조회된다. 이제 DISTINCT를 추가해보자.
select distinct t from Team t join fetch t.members where t.name = '팀A'
SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과에서 중복제거를 실패한다.
이제 DISTINCT가 추가로 애플리케이션에서 중복 제거를 시도한다.
select distinct t의 의미는 팀 엔티티의 중복을 제거하라는 것이다. 즉 같은 식별자를 가진 Team 엔티티를 제거한다.
페치 조인과 일반 조인을 정리하면
- 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회
- 페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념이다.
- JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
페치 조인 한계
- 페치 조인 대상에는 별칭을 줄 수 없다. (하이버네이트는 가능하지만 사용하지 말자.)
- 둘 이상의 컬렉션은 페치 조인할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
결론을 도출하자면 연관된 엔티티들을 SQL로 한 번에 조회할 때는 성능 최적화를 위해 페치 조인을 사용한다.
엔티티에 직접 적용하는 글로벌 로딩 전략보다 페치 조인이 우선된다.
실무에서 글로벌 로딩 전략은 모두 지연 로딩이라고 하며, 최적화가 필요한 곳은 페치 조인을 적용한다고 한다.
다형성 쿼리
아래 이미지와 같은 상속 관계가 존재한다고 생각해보자.
우리는 특정 자식을 조회하고 싶다. Item 중에 Book, Movie를 조회한다고 생각해보자.
그러면 type을 사용해 JPQL을 아래와 같이 짤 수 있다.
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 사용.
[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](JPQL 둘다 같은 다음 SQL 실행)
select count(m.id) as cnt from Member m
기본키와 마찬가지로 외래 키를 사용할 때도 엔티티를 사용하면 동일한 값을 얻을 수 있다.
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=?
Named 쿼리
미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을 Named 쿼리라 한다.
Named 쿼리는 한 번 정의하면 변경할 수 없는 정적인 쿼리다.
Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱 해둔다. 따라서 오류를 빨리 잡을 수 있다.
사용하는 시점에 파싱 된 결과를 재사용하므로 성능상 이점도 있다.
그리고 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스의 조회 성능 최적화에도 도움이 된다.
Named 쿼리는 어노테이션, XML로 정의할 수 있다. 어노테이션만 살펴보자.
@Entity
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class).setParameter("username", "회원1")
.getResultList();
벌크 연산
엔티티를 수정하려면 영속성 컨텍스트의 변경 감지 기능이나 병합을 사용하고, 삭제 메서드를 사용해야 한다.
이 방법으로 수많은 엔티티를 처리하기에는 시간이 너무 오래 걸린다.
이럴 때 수많은 엔티티를 한 번에 수정하거나 삭제하기 위해 벌크 연산을 사용한다.
UPDATE, DELETE를 지원하며 쿼리 한 번으로 여러 테이블 로우를 변경할 수 있다.
벌크 연산은 executeUpdate() 메소드를 사용한다. 이 메소드는 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다.
아래는 재고가 10개 미만인 모든 상품의 가격을 10% 상승시키는 벌크 연산 예제다.
String qlString = "update Product p " +
"set p.price = p.price * 1.1 " +
"where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(qlString).setParameter("stockAmount", 10)
.executeUpdate();
벌크 연산에도 주의할 점이 있다.
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 보낸다.
따라서 벌크 연산을 먼저 실행하고, 벌크 연산 수행 후 영속성 컨텍스트를 초기화해야 한다.
참고자료
자바 ORM 표준 JPA 프로그래밍 - 김영한
자바 ORM 표준 JPA 프로그래밍 강의 - https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
2023. 02.07 19:00 복습 및 정리 마무리
댓글