JPQL(Java Persistence Query Language)은 엔티티 객체를 조회하는 객체지향 쿼리다. JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다. 그리고 데이터베이스 방언만 변경하면 JPQL을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있다. 또한 JPQL은 SQL보다 간결하다.
//JPQL 사용
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
//실제 실행된 SQL
select
member.id as id,
member.age as age,
member.team_id as team,
member.name as name
from
Member member
where
member.name='kim'
- JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환된다.
기본 문법과 쿼리 API
JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다. 참고로 엔티티를 저장할 때는 persist() 메스드를 사용하면 되므로 INSERT 문은 없다.
SELECT 문
SELECT 문은 다음과 같이 사용한다
SELECT m FROM Member AS m where m.username = 'hello'
- 엔티티와 속성은 대소문자를 구분한다. 반면에 JPQL 키워드는 대소문자를 구분하지 않는다.
- JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티명이다. 엔티티 명은 @Entity로 따로 지정할 수 있다.
- JPQL은 별칭을 필수로 사용해야 한다. 별칭없이 작성하면 잘못된 문법이라는 오류가 발생한다.
TypeQuery, Query
작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다. 쿼리 객체는 TypeQuery와 Query가 있는데 반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 객체를 사용하고, 반환 타입을 명확하게 지정할 수 없으면 Query 객체를 사용하면 된다.
//TypeQuery 사용
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
//Query 사용
Query query = em.createQuery("SELECT m.username, m.age from Member m");
Query 객체는 SELECT 절의 조회 대상이 둘 이상이면 Object[]를 반환하고, 하나라면 Object를 반환한다. 두 코드를 비교해보면 타입을 변환할 필요가 없는 TypeQuery를 사용하는 것이 편리한 것을 알 수 있다.
결과 조회
다음 메소드들을 호출하면 실제 쿼리를 실행해서 데이터베이스를 조회한다.
- query.getResultList() : 결과를 예제로 반환한다. 만약 결과가 없으면 빈 컬렉션을 반환한다.
- query.getSingleResult() : 결과가 정확히 하나일 때 사용한다. 결과가 없거나 1개보다 많다면 예외가 발생한다.
파라미터 바인딩
JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩도 지원한다
이름 기준 파라미터
이름 기준 파라미터는 파라미터를 이름으로 구분하는 방법이다. 이름 기준 파라미터는 앞에 : 를 사용한다.
// 이름 기준 파라미터
List<Member members =
em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class)
.setParameter("username", usernameParam)
.getResultList();
위치 기준 파라미터
위치 기준 파라미터를 사용하려면 ? 다음에 위치 값을 주면 된다. 위치 값은 1부터 시작한다.
위치 기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.
프로젝션
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 하고 [SELECT {프로젝션 대상} FROM]으로 대상을 선택한다. 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다.
엔티티 프로젝션
SELECT m FROM Member m // 회원
SELECT m.team FROM Mebmer m // 팀
원하는 객체를 바로 조회한 것이다. 컬럼을 하나하나 나열해서 조회해야 하는 SQL과는 차이가 있다. 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
임베디드 프로젝션
String query = "SELECT o.address FROM Order o";
List<Address> adresses = em.createQuery(query, Address.class).getResultList();
JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용되지만 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다. 임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.
스칼라 타입 프로젝션
List<String> username = em.createQuery("SELECT username FROM Member m", String.class).getREsultList();
숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다. 중복데이터를 제거하려면 DISTINCT를 사용한다.
여러 값 조회
엔티티를 대상으로 조회하면 편리하겠지만, 꼭 필요한 데이터들만 선택해서 조회해야 할 때도 있다. 프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신에 Query를 사용해야 한다.
//여러 프로젝션 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.member, o.product, o.orderAmount FROM Order o")
.getResultList();
for (Object[] row : resultList) {
Member member = (Member) row[0];
Product product = (Product) row[1];
Integer orderAmount = (Integer) row[2];
}
New 명령어
위 예제에서 두 필드를 프로젝션해서 타입을 지정할 수 없으므로 TypeQuery를 사용할 수 없어 Object[]를 반환받았다. 실제 애플리케이션 개발시에는 Object[]를 직접 사용하지 않고 의미 있는 객체로 변환해서 사용할 것이다.
//UserDTO
public class UserDTO {
private String username;
private int age;
public UserDTO (String username, int age) {
this.usename = username;
this.age = age;
}
//...
}
//NEW 명령어 사용
TypedQuery<UserDTO> query =
em.createQuery("SELECT new jpanook.jpql.UserDTO(m.username, m.age) FROM Member m",
UserDTO.class);
List<UserDTO> ResultList = query.getResultList();
SELECT 다음에 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있는데 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다. 그리고 NEW 명령어를 사용한 클래스로 TypeQuery를 사용할 수 있어서 지루한 객체 변환 작업을 줄일 수 있다.
NEW 명령어를 사용할 때는 다음 2가지를 주의해야 한다.
- 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
- 순서와 타입이 일치하는 생성자가 필요하다.
페이징 API
페이징 처리용 SQL을 작성하는 일은 지루하고 반복적이며, 더 큰 문제는 데이터베이스마다 페이징을 처리하는 SQL 문법이 다르다는 점이다. JPA는 페이징을 다음 두 API로 추상화했다.
- setFirstResult (int startPosition) : 조회 시작 위치 (0부터 시작)
- setMaxResults(int maxResult) : 조회할 데이터 수
TypeQuery<Member> query =
em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC",
Member.class);
query.setFirstResult(10);
query.setMaxResults(20);
query.getResultList(); // 11~30번 데이터 조회
데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언 덕분이다. 실행된 페이징 SQL을 보면 실무에서 작성한 것과 크게 다르지 않지만, 페이징 SQL을 더 최적화하고 싶다면 JPA가 제공하는 페이징 API가 아닌 네이티브 SQL을 직접 사용해야 한다.
집합과 정렬
집합은 집합함수와 함께 통계 정보를 구할 때 사용한다.
집합 함수
함수 | 설명 |
COUNT | 결과 수를 구한다. 반환 타입: LONG |
MAX, MIN | 최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용한다. |
AVG | 평균값을 구한다. 숫자타입만 사용할 수 있다. 반환타입: Double |
SUM | 합을 구한다. 숫자타입만 사용할 수 있다. 반환 타입: 정수합 Long, 소수합: Double, BigInteger합: BigInteger, BigDecimal합: BigDecimal |
- NULL 값은 무시하므로 통계에 잡히지 않는다(DISTINCT가 정의되어 있어도 무시된다).
- 만약 값이 없는데 집합 함수를 사용하면 NULL이 된다. 단 COUNT는 0이 된다.
- DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다. ex . select COUNT(DISTINCT m.age) from Member m
- DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.
GROUP BY, HABING
GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다. 다음은 팀 이름을 기준으로 그룹별로 묶어서 통계 데이터를 구한다. HAVING은 GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링한다. 문법은 다음과 같다.
- group_절 ::= GROUP BY {단일값 경로 | 별칭}+
- having_절 ::= HAVING 조건식
//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
//HAVAING 예제
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
HABING AVG(m.age) >= 10
이런 쿼리들은 보통 리포팅 쿼리나 통계 쿼리라 한다. 이러한 통계 쿼리를 잘 활용하면 애플리케이션으로 수십 라인을 작성할 코드도 단 몇줄이면 처리할 수 있다. 하지만 통계 쿼리는 보통 전체 테이터를 기준으로 처리하므로 실시간으로 사용하기엔 부담이 많다. 결과가 아주 많다면 통계 결과만 저장하는 테이블을 별도로 만들어 두고 사용자가 적은 새벽에 통계 쿼리를 실행해서 그 결과를 보관하는 것이 좋다.
정렬(ORDER BY)
ORDER BY는 결과를 정렬할 때 사용한다. 문법은 다음과 같다.
- orderby절 ::= ORDER BY {상태필드 경로 | 결과 변수 [ASC | DESC]}+
select m from Member m order by m.age DESC, m.username ASC
JPQL 조인
JPQL도 조인을 지원하는데 SQL 조인과 기능은 같고 문법만 약간 다르다.
내부 조인
내부 조인은 INNER JOIN을 사용한다. 참고로 INNER는 생략할 수 있다.
String teamName = "팀A";
String query = "SELECT m FROM Member m INNER JOIN m.team t"
+ "WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
.setParamete("teamName", teamName)
.getResultList();
JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다. 연관 필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말한다.
외부 조인
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
외부 조인은 기능상 SQL의 외부 조인과 같다. OUTER는 생략 가능해서 보통 LEFT JOIN으로 사용한다.
컬렉션 조인
일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.
- [회원 -> 팀]으로의 조인은 다대일 조인이면서 단일 값 연관 필드(m.team)를 사용한다.
- [팀 -> 회원]은 반대로 일대다 조인이면서 컬렉션 값 연관 필드(t.members)를 사용한다.
SELECT t, m FROM Team t LEFT JOIN t.members m // 컬렉션 값 연관필드로 외부 조인
세타 조인
WHERE 절을 사용해서 세타 조인을 할 수 있다. 참고로 세타 조인은 내부 조인만 지원한다. 세타 조인을 사용하면 전혀 관계없는 엔티티도 조인할 수 있다.
//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)
JPA 2.1부터 조인할 때 ON 절을 지원한다. ON 절을 사용하면 조인 대상을 필터링하고 조인할 수 있다. 참고로 내부 조인의 ON 절은 WHERE절을 사용할 때와 결과가 같으므로 보통 ON 절은 외부 조인에서만 사용한다.
//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'
SQL 결과를 보묜 조인 시점에 조인 대상을 필터링한다.
페치 조인
페치 조인은 SQL에서 이야기하는 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 이것은 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능인데 join fetch 명령어로 사용할 수 있다. JPA 표준 명세에 정의된 페치 조인 문법은 다음과 같다.
- 페치조인 ::= [ 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
join 다음에 fetch라고 적게 되면 연관된 엔티티나 컬렉션을 함께 조회한다. 참고로 일반적인 JPQL 조인과는 다르게 m.team 다음에 별칭이 없는데 페치 조인은 별칭을 사용할 수 없다. 하지만 하이버네이트는 페치 조인에도 별칭을 허용한다.
엔티티 페치 조인 JPQL에서 select m으로 회원 엔티티만 선택했는데 실행된 SQL을 보면 회원과 연관된 팀도 함께 조회된 것을 확인할 수 있다. 그리고 회원과 팀 객체가 객체 그래프를 유지하면서 조회된 것을 확인할 수 있다.
String jpql = "select m from Member m join fetch m.team";
List<Memeber> members = em.createQuery(jpql, Member.class)
.getResultList();
for (Memeber member : members) {
//페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 발생 안 함
System.out.println("username = " + member.getUsername() + ", " +
"teamname = " + member.getTeam().name());
}
회원과 팀을 지연 로딩으로 설정했다고 가정했을 때 회원을 조회할 때 페치 조인을 사용해서 팀도 함께 조회했으므로 연관된 팀 엔티티는 프록시가 아닌 실제 엔티티다. 따라서 연관된 팀을 사용해도 지연 로딩이 일어나지 않는다. 그리고 프록시가 아닌 실제 엔티티이므로 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.
컬렉션 페치 조인
//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을 보면 팀과 연관된 회원도 함께 조회한 것을 확인할 수 있다. 그리고 TEAM 테이블에서 '팀A'는 하나지만 MEMBER 테이블과 조인하면서 결과가 증가해서 조인 결과 테이블을 보면 같은 '팀A'가 2건 조회되었다. 따라서 주소가 같은 '팀A'를 2건 가지게 된다.
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);
}
}
페치 조인과 DISTINCT
SQL의 DISTINCT는 중복된 결과를 제거하는 명령어다. JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론이고 애플리케이션에서 한 번 더 중복을 제거한다.
select distinct t from Team t join fetch t.members where t.name = '팀A'
먼저 DISTINCT를 사용하면 SQL에도 DISTINCT가 추가되지만 각 로우의 데이터가 다르므로 효과가 없다. 다음으로 애플리케이션에서 distinct 명령어를 보고 중복된 데이터를 걸러낸다. select distinct t의 의미는 팀 엔티티의 중복을 제거하라는 것이다. 따라서 중복인 팀은 하나만 조회된다.
페치 조인과 일반 조인의 차이
//내부 조인 JPQL
select t from t join t.members m where t.name = '팀A'
//SQL
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.D=M.TEAM_ID
WHERE T.NAME = '팀A'
페치 조인을 사용하지 않고 조인만 사용하면 팀만 조회하고 조인했던 회원은 전혀 조회하지 않는다. JPQL은 결과를 반환연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다. 따라서 팀 엔티티만 조회하고 연관된 회원 컬렉션은 조회하지 않는다. 먄약 회원 컬렉션을 지연 로딩으로 설정하면 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환한다. 즉시 로딩으로 설정하면 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한 번 더 실행한다.
페치 조인의 특징과 한계
페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다. 엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라 부른다. 페치 조인은 글로벌 로딩 전략보다 우선한다. 예를들어 글로벌 로딩 전략을 지연 로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치 조인을 적용해서 함께 조회한다.
최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에서 항상 즉시 로딩이 일어난다. 물론 일부는 빠를 수는 있지만 전체로 보면 사용하지 않는 엔티티를 자주 로딩하므로 오히려 성능에 악형양을 미칠 수 있다. 따라서 글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다. 또한 페치 조인을 사용하면 연관된 엔티티를 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않는다. 따라서 준영속 상태에서도 객체 그래프를 탐색할 수 있다.
페치 조인은 다음과 같은 한계가 있다.
- 페치 조인 대상에는 별칭을 줄 수 없다.
- 둘 이상의 컬렉션을 페치할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
페치 조인은 성능 최적화에 유용하여 실무에서 자주 사용하게 되지만 모든 것을 페치 조인으로 해결할 수는 없다. 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다. 반면에 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 억지로 페치 조인을 사용하기보다는 여러 테이블에서 필요한 필드들만 조회해서 DTO로 반환하는 것이 더 효과적일 수 있다.
경로 표현식
경로표현식이라는 것은 쉽게 이야기해서 .(점)을 찍어 객체 그래프를 탐색하는 것이다. 경로 표현식을 이해하려면 우선 다음 용어들을 알아야 한다.
-상태 필드: 단순히 값을 저장하기 위한 필드(필드 or 프로퍼티)
-연관 필드: 연관관계를 위한 필드, 임베디드 타입 포함(필드 or 프로퍼티)
-단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티
-컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "name")
private String username; // 상태 필드
private Integer age; // 상태 필드
@ManyToOne(..)
private Team team; // 연관 필드(단일 값 연관 필드)
@OneToMany(..)
private List<Order> orders; // 연관 필드(컬렉션 값 연관 필드)
JPQL에서 경로 표현식을 사용해서 경로 탐색을 하려면다음 3가지 경로에 따라 어떤 특징이 있는지 이해해야 한다.
- 상태 필드 경로: 경로 탐색의 끝이다. 더는 탐색할 수 없다.
- 단일 값 연관 경로: 묵시적으로 내부 조인이 일어난다. 단일 값 연관 경로는 계속 탐색할 수 있다.
- 컬렉션 값 연관경로: 묵시적으로 내부 조인이 일어난다. 더는 탐색할 수 없다. 단 FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있다.
-상태 필드 경로 탐색
//JPQL
select m.username, m.age from Member m
//SQL
select m.name, 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
단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이것을 묵시적 조인이라고 한다. 참고로 묵시적 조인은 모두 내부 조인이다. 외부조인은 명시적으로 JOIN 키워드를 사용해야 한다.
-컬렉션 값 연관 경로 탐색
JPQL을 다루면서 많이 하는 실수 중 하나는 컬렉션 값에서 경로 탐색을 시도하는 것이다. 컬렉션까지는 경로 탐색이 가능하다. 하지만 컬렉션에서 경로 탐색을 시작하는 것은 허락하지 않는다. 만약 컬렉션에서 경로 탐색을 하고 싶으면 조인을 사용해서 새로운 별칭을 획득해야 한다.
select m.username from Team t join t.members m
참고로 컬렉션은 컬렉션의 크기를 구할 수 있는 size라는 특별한 기능을 사용할 수 있다. size를 사용하면 COUNT 함수를 사용하는 SQL로 적절히 변환된다.
select t.members.size from Team t
경로 탐색을 사용한 묵시적 조인 시 주의사항
경로 탐색을 사용하면 묵시적 조인이 발생해서 SQL에서 내부 조인이 일어날 수 있다. 이 때 주의사항은 다음과 같다.
- 항상 내부 조인이다.
- 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 한다.
- 경로 탐색은 주로 SELECT, WHERE절 에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM 절에 영향을 준다.
조인이 성능상 차지하는 부분은 아주 크다. 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵다는 단점이 있다. 따라서 단순하고 성능에 이슈가 없으면 크게 문제가 안 되지만 성능이 중요하면 분석하기 쉽도록 묵시적 조인보다는 명시적 조인을 사용하자.
서브쿼리
JPQL도 SQL처럼 서브쿼리를 지원한다. 여기에는 몇 가지 제약이 있는데, 서브쿼리를 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용할 수 없다.
/** 서브쿼리 예시 **/
// 나이가 평균 보다 많은 회원찾기
select m from Member m
where m.age > (select avg(m2.age) from Member m2)
// 한 건이라도 주문한 고객 찾기
select m from Member m
where (select count(o) from Order o where m = o.member) > 0
// 위 예제 서브쿼리 대신 size 사용
select m from Member m
where m.orders.size > 0
서브쿼리 함수
-[NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참이다. NOT은 반대
-{ALL | ANY | SOME} (subquery): 비교 연산자와 같이 사용한다.
-ALL: 조건을 모두 만족하면 참이다
-ANY 혹은 SOME: 둘은 같은 의미다. 조건을 하나라도 만족하면 참이다.
-[NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참이다. 참고로 IN은 서브쿼리가 아닌 곳에서도 사용한다.
//EXIST 예제: 팀A 소속인 회원
select m from Member m
where exist (select t from m.team t where t.name = '팀A')
//ALL 예제: 전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o
where o.orderAmount > ALL (select p.stockAmount from Product p)
//ANY 예제: 어떤 팀이든 팀에 소속된 회원
select m from Member m
where m.team = ANY (select t from Team t)
//IN 예제: 20세 이상을 보유한 팀
select t from Team t
where t IN (select t2 From Team t2 JOIN t2.members m2 where m2.age >= 20)
다형성 쿼리
JPQL로 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회한다.
//JPQL
List resultList = em.createQuery("select i from Item i").getResultList();
//단일 테이블 전략 SQL
SELECT * FROM ITEM
//조인 전략 SQL
SELECT
i.ITEM_ID, i.DTYPE, i.name, i.price, i.stockQuantity,
b.author, b.isbn,
a.artist, a.etc,
m.actor, m.director
FROM
Item i
left outer join
Book b on i.ITEM_ID=b.ITEM_ID
left outer join
Album a on i.ITEM_ID=a.ITEM_ID
left outer join
Movie m on i.ITEM_ID=m.ITEM_ID
TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 사용한다.
//JPQL
select i from Item i
where type(i) IN (Book, Movie)
//SQL
SELECT i FROM Item i
WHERE i.DTYPE in ('B', 'M')
TREAT는 JPA 2.1에 추가된 기능인데 자바의 타입 캐스팅과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. JPA 표준은 FROM, WHERE 절에서 사용할 수 있지만, 하이버네이트는 SELECT 절에서도 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'
Named 쿼리: 정적 쿼리
JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있다.
- 동적 쿼리: em.createQuery("select ..")처럼 JPQL을 문자로 완성해서 직접 넘기는 것을 동적 쿼리라 한다. 런타임에 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다.
- 정적 쿼리: 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을 Named 쿼리라 한다. Named 쿼리는 한 번 정의하면 변경할 수 없는 정적인 쿼리다.
Name 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해둔다. 따라서 오류를 빨리 확인할 수 있고, 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다. 그리고 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스의 조회 성능 최적화에도 도움이 된다. Name 쿼리는 @NameQuery 어노테이션을 사용해서 자바 코드에 작성하거나 또는 XML 문서에 작성할 수 있다.
Named 쿼리를 어노테이션에 정의
Named 쿼리는 이름 그대로 쿼리에 이름을 부여해서 사용하는 방법이다.
//@NamedQuery 어노테이션을 사용해서 Named 쿼리 정의
@Entity
@NamedQuery(
name = "Member.findByUsername", // 충돌 방지를 위하여 엔티티 명을 앞에 붙힘
query = "select m from Member m where m.username = :username")
public class Member {
...
}
//@NamedQuery 사용
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
Named 쿼리를 사용할 때는 위 예제와 같이 em.createNamedQuery() 메소드에 Named 쿼리 이름을 입력하면 된다. 하나의 엔티티에 2개 이상의 Named 쿼리를 정의하려면 @NamedQueries 어노테이션을 사용하면 된다.
Named 쿼리를 XML에 정의
JPA에서 어노테이션으로 작성할 수 있는 것은 XML로도 작성할 수 있다. 보통 어노테이션을 사용하는 것이 직관적이고 편리하지만 Named 쿼리를 작성할 때는 XML을 사용하는 것이 더 편리하다. 자바 언어로는 멀티라인 문자를 다루는 것이 상당히 귀찮기 때문에 이런 불편함을 해결하려면 XML을 사용하는 것이 현실적인 대안이다.
<!--META-INF/ormMember.xml, XML에 정의한 Named 쿼리-->
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/us/persistence/orm" version="2.1">
<named-query name="Member.findByUsername">
<query><CDATA[
select m
from Member m
where m.username = :username
]></query>
</named-query>
<named-query name="Member.count">
<query>select count(m) from Member m</query>
</named-query>
</entity-mappings>
그리고 정의한 ormMember.xml을 인식하도록 META-INF/persistence.xml에 다음 코드를 추가해야 한다.
<persistence-unit name="jpabook">
<mapping-file>META-INF/ormMember.xml</mapping-file>
...
만약 XML과 어노테이션에 같은 설정이 있으면 XML이 우선권을 가진다.
'JPA' 카테고리의 다른 글
[JPA] 객체지향 쿼리 언어(2) : QueryDSL (0) | 2020.10.05 |
---|---|
[JPA] 값 타입 (0) | 2020.09.23 |
[JPA] 프록시와 연관관계 관리 (0) | 2020.09.22 |
[JPA] 고급 매핑 (0) | 2020.09.15 |
[JPA] 연관관계 매핑 (0) | 2020.09.10 |