JPQL 조인
- 내부 조인 (INNERT JOIN)
String teamName = "teamA";
String query = "SELECT m FROM Member m INNER JOIN m.team t "
+ "WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
.setParameter("teamName", teamName)
.getResultList();
- JPQL은 조인을 할때도 연관관계 필드를 사용함
String SQL = "SELECT M.ID, M.AGE ... FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE T.NAME = ?
"
String JPQL = "select m FROM Member m INNER JOIN m.team t
WHERE t.name = ?"
- 외부 조인 (OUTER JOIN)
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
- OUTER는 생략 가능
- 컬렉션 조인
- 일대다 또는 다대다 관계처럼 컬렉션을 사용하는 곳에서 조인을 할 때
// 컬렉션 값 연관 필드로 외부 조인
SELECT t, m FROM Team t LEFT JOIN t.members m
- 세타 조인
- 전혀 관계없는 엔티티도 조인할 수 있음
//JPQL
SELECT COUNT(m) FROM Member m, Team t WHERE m.username = t.name
//SQL
SELECT COUNT(M.ID)
FROM
MEMBER M CROSS JOIN TEAM T
WHERE
M.USERNAME = T.NAME
- JOIN ON 절(JPA 2.1)
- 조인 대상을 필터링 할 때
//JPQL
SELECT m, t FROM Member m LEFT JOIN m.team t ON t.name ="A"
//SQL
SELECT m.* t.* FROM Member m
LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name = "A"
페치(fetch) 조인
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 한 번에 같이 조인하는 기능
- 동적으로 사용하는 즉시로딩인데 LAZY, EAGER와는 다르게 연관 엔티티도 하나의 쿼리로 함께 가져옴 (N+1 문제 해결)
- 페치조인은 별칭(as) 사용이 불가능 (하이버네이트는 별칭 사용 지원 함)
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
- 회원과 팀을 지연로딩으로 설정헀어도 쿼리 결과는 프록시가 아닌 실제 엔티티가 됨
- 컬렉션 페치조인
// JPQL
select m from Team t join fetch t.members where t.name="teamA"
// SQL
SELECT
T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME="teamA"
- 팀과 멤버는 일대다 관계, 위와 같이 컬렉션 조인을 할 경우 다음과 같은 결과
- 팀 테이블에서 “teamA”는 하나지만 조회 결과 2건으로 결과가 증가함
- 이처럼 일대다 조인은 결과가 증가할 수 있음 (일대일, 다대일은 증가 안함)
- 페치 조인과 DISTINCT
- 일대다 조인을 할 때 결과가 늘어나는 상황에서 중복을 제거할 때 사용
- 위에서 다대일 관계에서 컬렉션을 조회하면 조회 결과는 아래와 같이 여러개의 리스트가 반환됨
- 같은 Team 엔티티가 중복으로 반환된 상황
- JPQL의 DISTINCT 명령어는 SQL에서도 DISTINCT를 처리하고 애플리케이션에서도 이런 상황에서 중복을 제거해줌
select distinct t
from Team t join fetch t.members
where t.name = "teamA"
- SQL의 DISTINCT 결과는 왼쪽과 같이 차이가 없음(중복된 데이터가 없으므로)
- 애플리케이션의 입장에서는 오른쪽과 같이 같은 식별자를 가진 중복 엔티티가 제거된 상태로 반환됨
- 페치 조인과 일반 조인의 차이
- 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음
- JPQL은 결과를 반환할 때 연관관계를 고려하지 않음
- 단지 SELECT 절에 지정한 엔티티만 조회함
- 프록시나 아직 초기화되지 않은 컬렉션 래퍼를 반환, 후에 지연로딩으로 사용 시에 조회
- 페치 조인은 연관 엔티티를 쿼리 시점에 조회하기 때문에 지연 로딩이 발생하지 않음
- 따라서 준영속 상태에서도 객체 그래프 탐색이 가능함
- 페치 조인은 연관된 엔티티도 함께 조회(즉시로딩)
- 즉시로딩과는 또 다른게 SQL 한번에 연관된걸 다 조회할 수 있음
- 페치 조인의 특징과 한계
- 페치 조인 대상에는 별칭을 줄 수 없음
- 하이버네이트는 지원하지만 사용하지 않을 것을 권장
- 연속적인 페치 조인을 하는 경우가 아니면 사용해선 안됨
- 왜 사용하면 안되냐?
select t from Team t join fetch t.members m where m.age > 10
- 이런 쿼리를 사용한다고 했을 때 m.age가 10 이상인 컬렉션만 반환됨
- 연관된 모든 members 엔티티가 아닌 일부 엔티티만 가져오게 되고 만약 이게 2차 캐시랑 같이 사용되면 다른 엔티티가 members 연관관계를 조회해도 캐싱된 개수를 가져갈 것
- 엔티티를 조회할 때 연관관계는 모두 가져오는 것이 객체 그래프 탐색의 사상?
- 둘 이상의 컬렉션은 페치 조인 할 수 없음
- 컬렉션 * 컬렉션은 Cartesian 곱.. 결과가 엄청 많아질 수 있기 때문에 제한
- 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없음
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능(DISTINCT 부분에서 설명했듯이 일대다 관계는 중복된 데이터가 발생하기 때문에)
- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징함 (매우 위험..)
벌크 연산
- 엔티티를 수정하려면 영속성 컨텍스트의 변경감지 또는 병합을 사용
- 만약 수백개 이상의 엔티티를 수정해야할 일이 생긴다면?
- 변경감지로 엔티티 하나하나 업데으트를 하려면 많은 수의 쿼리 발생
- 여러 건을 한번에 처리하는 쿼리를 벌크 연산이라고 함
- 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();
- JPA 표준은 아니지만 하이버네이트는 INSERT 연산에 대해서도 벌크 연산 가능
- 벌크 연산시 주의점
- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 하는 것
- JPQL 처럼 쿼리 시 영속성 컨텍스트의 내용이 flush되지만 쿼리 후에 데이터베이스의 상태와 영속성 컨텍스트가 동기화되지 않음
- 방법 1. 영속성 컨텍스트를 사용하기 전에 벌크 연산을 먼저 한다
- 방법 2. 벌크 연산을 수행한 다음에는 영속성 컨텍스트를 초기화해준다
- 방법 3. 벌크 연산 후에
em.refresh(entity)
를 통해 엔티티를 다시 조회함
- 영속성 컨텍스트와 JPQL
- JPQL로 쿼리 후에 영속성 컨텍스트가 관리하는 것은 오직 엔티티 타입만!
select m from Member m // 엔티티 조회 - 영속성 컨텍스트에 관리됨
select o.address from Order o // 임베디드 타입 조회 (관리 X)
select m.id, m.username from Member m // 단순 필드 조회 (관리 X)
- 예를들어 임베디드 타입(o.address)를 조회하고 수정해도 변경 감지에 의한 수정은 되지 않음
- 영속성 컨텍스트에 이미 있는 엔티티를 JPQL로 다시 조회한다면?
- 조회 결과를 버리고 영속성 컨텍스트에 있는 엔티티를 반환함
- find() vs JPQL
- find()는 영속성 컨텍스트의 1차 캐시에 엔티티가 있으면 조회하지 않고 반환함
- JPQL은 무조건 데이터베이스 쿼리, 1차 캐시에 엔티티가 있으면 반환, 없으면 1차 캐시로 영속성 컨텍스트에서 관리ㅑ