JPQL
- JPQL의 특징
- JPQL은 객체지향 쿼리 언어이므로 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리
- JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음
- JPQL은 결국 SQL로 변환됨
- 샘플 도메인 모델 UML, ERD
1) 회원이 상품을 주문하는 다대다 관계를 일다대, 다대일 관계로 풀어내기 위해 연결 엔티티인 ORDER 사용
2) Address는 임베디드 타입이므로 UML에 스테레오 타입을 사용해 <<Value>>로 정의하고 ERD에는 ORDERS 테이블에 포함
기본 문법과 쿼리 API
- JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있으며
엔티티를 저장할 때는 EntityManager.persist() 메소드를 사용하면 되므로 INSERT 문은 없음
// JPQ 문법
// 전체 구조는 SQL과 비슷
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
// JPQL에서 UPDATE, DELETE 문은 벌크 연산이라고 함
update_문 :: == update_절 [where_절]
delete_문 :: == delete_절 [where_절]
- SELECT문
- 대소문자 구분
엔티티와 속성은 대소문자를 구분함 (SELECT, FROM, AS와 같은 JPQL 키워드는 대소문자를 구분하지 않음) - 엔티티 이름
JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명 - 별칭은 필수
Member AS m으로 Member에 m라는 별칭을 주어 사용해야만 하며 별칭 없이 작성할 경우 잘못된 문법이라는 오류 발생
- 대소문자 구분
// SELECT문 사용
SELECT m FROM Member AS m Where m.username = 'Hello'
// 별칭이 없어 오류 발생
SELECT username FROM Member m // 잘못된 문법, username을 m.username으로 고쳐야 함
- TypeQuery, Query
작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 하는데 쿼리 객체는 TypeQuery와 Query가 있음
- 반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 객체를 사용
- 그렇지 않으면 Query 객체 사용
// TypeQuery 사용
// em.creatQuery()의 두 번째 파라미터에 반환할 타입을 지정하면 TypeQuery를 반환
// 조회할 대상이 Member 엔티티이므로 조회 대상 타입이 명확해 TypeQuery 사용
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = query.gerResultList();
for (Member member : resultList) {
System.out.println("member = " + member);
}
// Query 사용
// em.creatQuery()의 두 번째 파라미터에 반환할 타입을 지정하지 않으면 Query를 반환
// 조회 대상이 String 타입인 회원 이름과 Integer 타입인 나이이므로 조회 대상 타입이 명확하지 않음
// SELECT 절에서 여러 엔티티나 컬럼을 선택할 때는 반환할 타입이 명확하지 않으므로 Query 객체 사용
Query query = em.createQuery("SELECT m.username, m.age from Member m");
List resultList = query.getResultList();
for (Object o : resultList) {
// Query 객체는 SELECT 절의 조회 대상이 둘 이상이면 Object[]를 반환하고
// SELECT 절의 조회 대상이 하나면 Object를 반환
Object[] result = (Object[]) o;
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
- 결과 조회
메소드들을 호출하여 실제 쿼리를 실행해서 데이터베이스를 조회
- query.getResultList()
결과를 예제로 반환하며 만약 결과가 없으면 빈 컬렉션을 반환 - query.getSingleResult()
결과가 정확히 하나일 때 사용하며 결과가 없거나 1개보다 많으면 예외가 발생함
- query.getResultList()
Member member = query.getResultList();
Member member = query.getSingleResult();
파라미터 바인딩
- JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩도 지원
- 이름 기준 파라미터
이름 기준 파라미터는 파라미터를 이름으로 구분하는 방법이며 이름 기준 파라미터는 앞에 :를 사용
// 이름 기준 파라미터 사용
String usernameParam = "User1";
// :username이라는 이름 기준 파라미터를 정의하고
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class);
// username이라는 이름으로 파라미터를 바인딩
query.setParameter("username", usernameParam);
List<Member> resultList = query.getResultList();
// 이름 기준 파라미터를 메소드 체인 방식으로 작성
List<Member> members = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class)
.setParameter("username", usernameParam)
.getResultList();
- 위치 기준 파라미터
위치 기준 파라미터를 사용하려면 ? 다음에 위치 값을 주면 되며 위치 값은 1부터 시작
// 위치 기준 파라미터 사용
List<Member> members = em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class)
.setParameter(1, usernameParam)
.getResultList();
- 위치 기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확
프로젝션
- SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라고 하고 [SELECT (프로젝션 대상) FROM]으로 대상을 선택
- 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 있음
- 엔티티 프로젝션
컬럼을 하나하나 나열해서 조회해야 하는 SQL와 달리
엔티티를 프로젝션 대상으로 사용할 수 있어 원하는 객체를 바로 조회가 가능하며, 조회한 엔티티는 영속성 컨텍스트에서 관리됨
SELECT m FROM Member m // 회원 엔티티를 프로젝션 대상으로 조회
SELECT m.team FROM Member m // 회원과 연관된 팀 엔티티를 프로젝션 대상으로 조회
- 임베디드 타입 프로젝션
JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용되지만
조회의 시작점이 될 수 없다는 제약이 있으므로 엔티티를 통해서 엠베디드 타입을 조회할 수 있음
임베디드 타입은 엔티티 타입이 아닌 값 타입이므로 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않음
// 임베디드 타입인 Address를 조회의 시작점으로 사용해서 잘못된 쿼리의 예
String query = "SELECT a FROM Address a";
// Order 엔티티를 시작점으로 하여 이 엔티티를 통해 임베디드 타입을 조회
String query = "SELECT o.address FROM Order o";
List<Address> addresses = em.createQuery(query, Address.class)
.getResultList();
// 실행된 SQL
select
order.city,
order.street,
order.zipcode
from
Order order
- 스칼라 타입 프로젝션
숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라타입이라고 하며 중복 데이터를 제거하려면 DISTINCT를 사용
통계 쿼리도 주로 스칼라 타입으로 조회함
// 스칼라 타입인 전체 회원의 이름을 조회
List<String> username = em.createQuery("SELECT username FROM Member m", String.class)
.getResultList();
// 중복 데이터를 제거하려면 DISTINCT를 사용
SELECT DISTINCT username FROM Member m
// 통계 쿼리도 주로 스칼라 타입으로 조회
Double orderAmountAvg = em.createQuery("SELECT AVG(o.orderAmount) FROM Oder o", Double.class)
.getSingleResult();
- 여러 값 조회
엔티티를 대상으로 조회하면 편리하겠지만, 꼭 필요한 데이터들만 선택해서 조회해야 할 때 사용하며
프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신에 Query를 사용해야 함
또한 제네릭에 Object[]를 사용하면 조금 더 간결하게 개발할 수 있음
스칼라 타입 뿐만 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있으며 조회한 엔티티는 영속성 컨텍스트에서 관리됨
// 여러 프로젝션
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();
Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
Object[] row = (Object[]) iterator.next();
String username = (String) row[0];
Integer age = (Integer) row[1];
}
// 제네릭에 Object[]를 사용하면 조금 더 간결하게 개발 가능
List<Object[]> resultList = em.createQuery("SELECT m.username, m.age FROM Member m")
.getResultList();
for (Object[] row : resultList) {
String username = (String) row[0];
Integer age = (Integer) row[1];
}
// 스칼라 타입 뿐만 아니라 엔티티 타입도 여러 값을 조회할 수 있음
List<Object[]> resultList = em.createQuery("SELECT o.username, o.product, o.orderAmount FROM Order o")
.getResultList();
for (Object[] row : resultList) {
Member member = (Member) row[0]; // 엔티티
Product product = (Product) row[1]; // 엔티티
int orderAmount = (Integer) row[2]; // 스칼라
}
- NEW 명령어
username, age 두 필드를 프로젝션해서 타입을 지정할 수 없으므로 TypeQuery를 사용할 수 없으므로
Object[]를 반환받는데 실제 애플리케이션 개발 시에는 Object[]를 직접 사용하지 않고 의미 있는 객체로 변환해서 사용
객체 변환 작업을 위해 SELECT 다음에 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있어
이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있으며
NEW 명령어를 사용한 클래스로 TypeQuery를 사용할 수 있어 지루한 객체 변환 작업을 줄일 수 있음
// userDTO
public class UserDTO {
private String username;
private int age;
public UserDTO(String username, int age) { // 클래스의 생성자
this.username = username;
this.age = age;
}
// ...
}
// NEW 명령어 사용 전
List<Object[]> resultList = em.createQuery("SELECT m.username, m.age FROM Member m")
.getResultList();
// 객체 변환 작업 (UserDTO처럼 의미 있는 객체로 변환)
List<UserDTO) useerDTOs = new ArrayList<UserDTO>();
for (Object[] row : resultList) {
UserDTO userDTO = new UserDTO((String)row[0], (Integer)row[1]);
userDTOs.add(userDTO);
}
return userDTOs;
// NEW 명령어 사용 후
// SELECT 다음에 NEW 명령어를 사용해 반환받을 클래스(UserDTO)를 지정한 후
// 클래스의 생성자에 JPQL 조회 결과를 넘겨주어 NEW 명령어를 사용한 클래스로 TypeQuery 사용 가능
/* NEW 명령어를 사용할 때의 2가지 주의점
1. 패키지 명을 포함한 전체 클래스 명을 입력해야 함
2. 순서와 타입이 일치하는 생성자가 필요 */
TypedQuery<UserDTO> query = em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
페이징 API
- 페이징 처리용 SQL을 작성하는 일을 지루하고 반복적이며 데이터베이스마다 페이징을 처리하는 SQL 문법이 다름
- 이를 위해 JPA는 페이징을 두 API로 추상화하였고 데이터베이스 방언 덕분에
데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있게 되며 방언에 따라 JPQL이 SQL로 변환됨
- setFirstResult(int startPosition) : 조회 시작 위치
- setMaxResults(int maxResult) : 조회할 데이터 수
// 페이징 사용
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);
// 11번째부터 시작해서 총 20건의 데이터를 조회하므로 11~30번 데이터를 조회
query.setFirstResult(10);
query.setMaxResults(20);
query.getResultList();
- 데이터베이스별 페이징 쿼리 결과
데이터베이스마다 SQL이 다른 것은 물론이고 오라클과 SQL Server는 페이징 쿼리는 매우 복잡
페이징 SQL을 더 최적화하고 싶다면 JPA가 제공하는 페이징 API가 아닌 네이티브 SQL을 직접 사용해야 함
-- HSQLDB (org.hibernate.dialect.HSQLDialect)
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M
ORDER BY
N.NAME DESC OFFSET ? LIMIT ?
-- MySQL (org.hibernate.dialect.MySQL5innoDBDialect)
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M
ORDER BY
N.NAME DESC LIMIT ?, ?
-- PostgreSQL (org.hibernate.dialect.PostgreSQLDialect)
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M
ORDER BY
N.NAME DESC LIMIT ? OFFSET ?
-- 오라클 (org.hibernate.dialect.Oracle10gDialect)
SELECT *
FROM
( SELECT ROW_.*, ROWNUM ROWNUM_
FROM
( SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM MEMBER M
ORDER BY M.NAME
) ROW_
WHERE ROWNUM <= ?
)
WHERE ROWNUM_ > ?
-- SQLServer (org.hibernate.dialect.SQLServer2008Dialect)
WITH query AS (
SELECT
inner_query.*,
ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as
__hibernate_row_nr__
FROM
( select
TOP(?) m.id as id,
m.age as age,
m.team_id as team_id,
m.name as name
from Member m
order by m.name DESC
) inner_query
)
SELECT id, age, team_id, name
FROM query
WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?
집합과 정렬
- 집합은 집합 함수와 함께 통계 정보를 구할 때 사용
-- 예) 회원수, 나이 합, 평균 나이, 최대 나이, 최소 나이를 조회
select
COUNT(m), // 회원수
SUM(m.age), // 나이 합
AVG(m.age), // 평균 나이
MAX(m.age), // 최대 나이
MIN(m.age) // 최소 나이
from Member m
- 집합 함수
함수 | 설명 |
COUNT | 결과 수를 구한다. 반환 타입 : Long |
MAX, MIN | 최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용한다. |
AVG | 평균값을 구한다. 숫자타입만 사용할 수 있다. 반환 타입 : Double |
SUM | 합을 구한다. 숫자 타입만 사용할 수 있다. 반환 타입 : 정수합 Long, 소수합 Double, BigInteger합 BigInteger, BigDecimal합 BigDecimal |
- 집합 함수 사용 시 참고사항
- NULL 값은 무시하므로 통계에 잡히지 않음
- 만약 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL 값이 되며, COUNT는 0이 됨
- DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있음
예) select COUNT (DISTINCT m.age) from Member m - DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않음
- GROUP BY, HAVING
GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어줌
HAVING은 GROUP BY와 함께 사용되며 GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링함
통계 쿼리를 잘 활용하면 애플리케이션으로 수십 라인을 작성할 코드도 단 몇 줄이면 처리할 수 있음
하지만 통계 쿼리는 보통 전체 데이터를 기준으로 처리하므로 실시간으로 사용하기엔 부담이 많으므로
결과가 아주 많다면 통계 결과만 저장하는 테이블을 별도로 만들어 두고 사용자가 적은 새벽에 통계 쿼리를 실행해서 결과를 보관
// 예) 팀 이름을 기준으로 그룹별로 묶어서 통계 데이터를 구함
// GROUP BY의 문법
// groupby_절 ::= GROUP BY {단일값 경로 | 별칭}+
select t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
from Member m LEFT JOIN m.team t
GROUP BY t.name
// 예) 위의 그룹별 통계 데이터 중에서 평균나이가 10살 이상인 그룹을 조회
// HAVING 문법
// having_절 ::= HAVING 조건식
select t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
from Member m LEFT JOIN m.team t
GROUP BY t.name
HAVING AVG(m.age) >= 10
- 정렬 (ORDER BY)
ORDER BY는 결과를 정렬할 때 사용
- ASC : 오름차순 (기본값)
- DESC : 내림차순
// 예) 나이를 기준으로 내림차순으로 정렬하고 나이가 같으면 이름을 기준으로 오름차순으로 정렬
// ORDER BY 문법
// orderby_절 ::= ORDER BY {상태필드 경로 | 결과 변수 [ASC | DESC]}+
select m from Member m order by m.age DESC, m.usernmae ASC
/* 상태필드란 t.name 같이 객체의 상태를 나타내는 필드를 뜻하며
결과 변수란 SELECT 절에 나타나는 값인 cnt을 뜻함
select t.name, COUNT(m.age) as cnt
from Member m LEFT JOIN m.team t
GROUP BY t.name
ORDER BY cnt */
JPQL 조인
- JPQL도 조인을 지원하며 SQL 조인과 기능은 같고 문법만 약간 다름
- 내부 조인
INNER JOIN을 사용하며 INNER는 생략할 수 있음
JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이며
아래에서는 m.team이 연관 필드인데 연관 필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 뜻 함
// 내부 조인 사용 예
// 회원과 팀을 내부 조인해서 '팀A'에 소속된 회원을 조회하는 SQL
String teamName = "팀A";
/* JPQL 조인은 연관 필드를 사용하며 여기서 m.team이 연관 필드
FROM Member m : 회원을 선택하고 m이라는 별칭을 줌
Member m JOIN m.team t : 회원이 가지고 있는 연관 필드로 팀과 조인한 후, 조인한 팀에 t라는 별칭을 줌
SQL 조인처럼 FROM Member m JOIN Team t로 조인할 객체의 연관 필드를 사용하지 않으면 오류 발생 */
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();
// 생성된 내부 조인 SQL
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
WHERE
T.NAME=?
// 조인 결과 활용
// '팀A' 소속인 회원을 나이 내림차순으로 정렬하고 회원명과 팀명을 조회하는 JPQL 쿼리
SELECT m.username, t.name
FROM Member m JOIN m.team t
WEHRE t.name = '팀A'
ORDER BY m.age DESC
// 조인한 두 개의 엔티티를 조회하는 JPQL 쿼리
SELECT m, t
FROM Member m JOIN m.team t
// 위의 경우 서로 다른 타입의 두 엔티티를 조회했으므로 TypeQuery를 사용할 수 없으므로
// 아래처럼 조회
List<Object[]> result = em.createQuery(query).getResultList();
for (Object[] row : result) {
Member member = (Member) row[0];
Team tema = (Team) row[1];
}
- 외부 조인
외부 조인은 기능상 SQL의 외부 조인과 같으며 OUTER는 생략이 가능해서 보통 LEFT JOIN으로 사용
// 외부 조인 JPQL
SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t
// 생성된 외부 조인 SQL
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M LEFT OUTER JOIN TEAM T ON M.TEAM_ID=T.ID
WHERE
T.NAME=?
- 컬렉션 조인
일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것
- [회원 -> 팀]으로의 조인은 다대일 조인이면서 단일 값 연관필드(m.team)를 사용
- [팀 -> 회원]은 반대로 일대다 조인이면서 컬렉션 값 연관필드(t.members)를 사용
// t LEFT JOIN t.members는 팀과 팀이 보유한 회원목록을 컬렉션 값 연관 필드로 외부 조인
SELECT t, m FROM TEAM t LEFT JOIN t.members m
- 세타 조인
WHERE 절을 사용해서 세타 조인을 할 수 있으며 세타 조인은 내부 조인만 지원
세타 조인을 사용하면 전혀 관계없는 엔티티도 조인할 수 있음
// 전혀 관련없는 Member.usernmae과 Team.name을 조인
// 회원 이름이 팀 이름과 똑같은 사람 수를 구하는 예
select count(m) from Member m, Team t
where m.usernmae = t.name
// 생성된 세타 조인 SQL
SELECT COUNT(M.ID)
FROM
MEMBER M CROSS JOIN TEAM T
WEHRE
M.USERNAME=T.NAME
- JOIN ON 절
JPA 2.1부터 조인할 때 ON 절을 지원하며 ON 절을 사용하면 조인 대상을 필터링하고 조인할 수 있음
내부 조인의 ON 절은 WHERE 절을 사용할 때와 결과가 같으므로 보통 ON 절은 외부 조인에서만 사용
// 모든 회원을 조회하면서 회원과 연관된 팀도 조회하는데 팀은 이름이 A인 팀만 조회
select m, t from Member m
left join m.team t on t.name = 'A'
// 생성된 JOIN ON 절 SQL
SELECT m.*, t.* FROM Member m
LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'
페치 조인
- 페치 조인은 조인의 종류가 아니라 JPQL에서 성능 최적화를 위해 제공하는 기능으로
연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이며 join fetch 명령어로 사용할 수 있음
페치 조인은 JPQL 조인과 다르게 별칭을 사용할 수 없는 특징을 가짐
SQL 한 번으로 연관된 여러 엔티티를 조회할 수 있어 성능 최적화에 유용하며, 객체 그래프를 유지할 때 사용하면 효과적
반면 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면
페치 조인을 사용하기보다는 여러 테이블에서 필요한 필드들만 조회해서 DTO로 반환하는 것이 더 효과적
// 페치 조인 문법
페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
- 엔티티 페치 조인
엔티티 페치 조인을 이용하면 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회 가능
// 페치 조인을 사용해서 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회하는 JPQL
select m
from Member m join fetch m.team
// 실행된 페치 조인 SQL
// 엔티티 페치 조인 JPQL에서 select m 으로 회원 엔티티만 선택했는데
// SQL을 보면 SELECT M.*, T.*로 회원과 연관된 팀도 함께 조회된 것을 확인할 수 있음
// 위의 엔티티 페치 조인 결과 객체를 보더라도 회원과 팀 객체가 객체 그래프를 유지하면서 조회된 것 확인
SELECT
M.*, T.*
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID
// 페치 조인 사용
/* 회원과 팀을 지연 로딩으로 설정했다고 가정했을 때
회원을 조회할 때 페치 조인을 사용해서 팀도 함께 조회했으므로
연관된 팀 엔티티는 프록시가 아닌 실제 엔티티이므로 연관된 팀을 사용해도 지연 로딩이 일어나지 않음
그러므로 팀 엔티티는 프록시가 아닌 실제 엔티티이므로
회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속성 상태가 되어도 연관된 팀을 조회할 수 있음 */
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
.getResultList();
for (Member member : members) {
// 패치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 발생 안 함
System.out.println("username = " + member.getUsernmae() + ", " + "teamname = " + member.getTeam().name());
}
// 출력 결과
username = 회원1, teamname = 팀A
username = 회원2, teamname = 팀A
username = 회원3, teamname = 팀B
- 컬렉션 페치 조인
일대다 관계인 컬렉션을 페치 조인하여 연관된 회원 컬렉션도 함께 조회 가능
// 컬렉션 페치 조인 JPQL
select t
from Team t join fetch t.members
where t.name = '팀A'
// 실행된 컬렉션 페치 조인 SQL
// 컬렉션을 페치 조인한 JPQL에서 select t로 팀만 선택했는데
// SQL을 보면 T.*, M.*로 팀과 연관된 회원도 함께 조회한 것을 확인할 수 있음
// 팀(t)을 조회하면서 페치 조인을 사용해서 연관된 회원 컬렉션(t.members)도 함께 조회
SELECT
T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
// 컬렉션 페치 조인 사용
String jpql = "select t from Team t join fetch t.members where t.name = '팀A'";
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
for (Team team : teams) {
System.out.println("teamname = " + team.getName() + ", team = " + team);
for (Member member : team.getMembers()) {
// 페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안 함
System.out.println("->username = " + member.getUsername()+ ", member = " + member);
}
}
// 출력 결과
// TEAM 테이블에서 '팀A'는 하나지만 MEMBER 테이블과 조인하면서 결과가 증가해서
// 조인 결과 테이블을 조면 같은 '팀A'가 2건 조회됨
teamname = 팀A, team = Team@0x100
->username = 회원1, member = Member@0x200
->username = 회원2, member = Member@0x300
teamname = 팀A, team = Team@0x100
->username = 회원1, member = Member@0x200
->username = 회원2, member = Member@0x300
- 페치 조인과 DISTINCT
SQL의 DISTINCT는 중복된 결과를 제거하는 명령어이며
JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것을 물론 애플리케이션에서 한 번 더 중복을 제거
위의 컬렉션 페치 조인의 경우 팀A가 중복으로 조회되는데 여기에 DISTINCT를 사용하면 SQL에 SELECT DISTINCT가 추가됨
SQL의 DISTINCT는 로우의 모든 속성이 같아야만 제거가 되는데 지금은 각 로우의 데이터가 다르므로 효과가 없음
반면 애플리케이션에서는 dinstinct 명령어를 보고 중복된 데이터를 걸러내므로
select distinct i의 의미인 팀 엔티티의 중복을 제거하라는 의미처럼 중복인 팀A는 하나만 조회됨
// 컬렉션 페치 조인 사용 예제에 DISTINCT 추가
select distinct t
from Team t join fetch t.members
where t.name '팀A'
// 출력 결과
teamname = 팀A, team = Team@0x100
->username = 회원1, member = Member@0x200
->username = 회원2, member = Member@0x300
- 페치 조인과 일반 조인의 차이
JPQL에서 팀과 회원 컬렉션을 조인했으므로 회원 컬렉션도 힘께 조회하기를 기대했지만,
페치 조인을 사용하지 않고 조인만 사용하면 JPQL은 결과를 반환할 때 연관관계까지 고려하지 않으므로
단지 SELECT 절에 지정한 엔티티만 조회할 뿐이며, 따라서 팀 엔티티만 조회하고 연관된 회원 컬렉션은 조회하지 않음
만약 회원 컬렉션을 지연 로딩으로 설정하면 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환하며,
즉시 로딩으로 설정하면 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한 번 더 실행함
반면에 페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티도 함께 조회 가능
// 내부 조인 JPQL (페치 조인 X)
select t
from Team t join t.members m
where t.name = '팀A'
// 실행된 SQL
// SELECT절에 지정한 엔티티인 팀 엔티티만 조회하고 연관된 회원 컬렉션은 조회하지 않음
SELECT
T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
// 컬렉션 페치 조인 SQL
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'
- 페치 조인의 특징
- 페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어 SQL 호출 횟수를 줄여 성능을 최적화
- 엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라고 부르며
페치 조인은 글로벌 로딩 전략보다 우선
예) 글로벌 로딩 전략을 지연 로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치 조인을 적용해서 함께 조회 - 최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에서 항상 즉시 로딩이 일어남
이는 일부는 빠를 수 있지만 전체로 보면 사용하지 않는 엔티티를 자주 로딩하므로 오히려 성능에 악영향을 미칠 수 있음
그러므로 글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요하다면 페치 조인을 적용하는 것이 효과적 - 페치 조인을 사용하면 연관된 엔티티를 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않으며
따라서 준영속 상태에서도 객체 그래프를 탐색할 수 있음
- 페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어 SQL 호출 횟수를 줄여 성능을 최적화
- 페치 조인의 한계
- 페치 조인 대상에는 별칭을 줄 수 없어 SELECT, WHERE 절, 서브 쿼리에 페치 조인 대상을 사용할 수 없음
- JPA 표준에서는 지원하지 않지만 하이버네이트를 포함한 몇몇 구현체들은 페치 조인을 별칭을 지원함
하지만 별칭을 잘못 사용하면 연관된 데이터 수가 달라져 데이터 무결성이 깨질 수 있음
특히 2차 캐시와 함께 사용할 경우 연관된 데이터 수가 달리진 상태에서 2차 캐시에 저장되면
다른 곳에서 조회할 때도 연관된 데이터 수가 달라지는 문제가 발생하므로 조심해야 함 - 둘 이상의 컬렉션을 페치 할 수 없음
구현체에 따라 되기도 하는데 컬렉션의 카테시안 곱이 만들어져 예외가 발생하므로 주의 - 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없으며
컬렉션(일대다)이 아닌 단일 값 연관 필드(일대일, 다대일)들은 페치 조인을 사용해도 페이징 API를 사용할 수 있음 - 하이버네이트에서는 컬렉션을 페치 조인하고 페이징 API를 사용하면 경고 로그를 남기지만 메모리에서 페이징 처리를 함
데이터가 적으면 상관없겠지만 데이터가 많으면 성능 이슈와 메모리 초과 예외가 발생할 수 있어 위험
'Java-Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 객체지향 쿼리 언어 ③ - Criteria (0) | 2022.05.03 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 객체지향 쿼리 언어 ② - JPQL (2) (0) | 2022.04.27 |
[자바 ORM 표준 JPA 프로그래밍] 객체지향 쿼리 언어 ① - 소개 (0) | 2022.04.21 |
[자바 ORM 표준 JPA 프로그래밍] 값 타입 - 실전 예제 (0) | 2022.04.20 |
[자바 ORM 표준 JPA 프로그래밍] 값 타입 (0) | 2022.04.19 |