1. 객체지향 쿼리 소개
EntityManager.find() 메서드를 사용하면 식별자로 엔티티 하나를 조회할 수 있다. 이렇게 조회한 엔티티에 객체 그래프 탐색을 사용하면 연관된 엔티티들을 찾을 수 있다. 이 둘은 가장 단순한 검색 방법이다.
- 식별자로 조회 : EntityManager.find()
- 객체 그래프 탐색 : ex) a.getB()
하지만 이 기능만으로 애플리케이션을 개발하기는 어렵다. ex) 나이가 30살 이상인 회원을 모두 검색하고 싶다.
그렇다고 모든 회원 엔티티를 메모리에 올려두고 검색하는 것은 현실성이 없다. 결국 데이터는 DB에 있으므로 SQL로 필요한 내용을 최대한 걸러서 조회해야 한다. 하지만 ORM을 사용하면 엔티티 객체를 대상으로 개발하므로 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요하다.
JPQL은 위와 같은 문제를 해결하기 위해 만들어졌다.
- 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼릳.
- SQL을 추상화해서 특정 DB SQL에 의존하지 않는다.
SQL이 테이블을 대상으로 하는 데이터 중심의 쿼리라면 JPQL은 엔티티 객체를 대상으로 하는 객체지향 쿼리다.
JPQL을 사용하면 JPA는 JPQL을 분석한 다음 적절한 SQL을 만들어 DB에서 조회한다.
- JPA가 공식 지원 하는 기능
- JPQL
- Criteria 쿼리 : JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
- 네이티브 SQL : JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다.
- JPA 가 공식 지원하는 기능은 아니지만 알아둘 필요가 있는 기능
- QueryDSL : Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크이다.
- JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용 : 필요하다면 JDBC를 직접 사용할 수 있다.
가장 중요한건 JPQL이다. Criteria나 QueryDSL은 JPQL을 편하게 작성하도록 도와주는 빌더 클래스일 뿐이다.
JPQL 소개
JPQL은 엔티티 객체를 조회하는 객체지향 쿼리다. JPQL은 SQL을 추상화해서 특정 DB에 의존하지 않는다. 또한 JPQL은 SQL보다 간결하다.
JPQL에서 Member는 엔티티 이름이다. 그리고 m.username은 테이블 컬럼명이 아니라 엔티티 객체의 필드명이다. em.createQuery() 메서드에 실행할 JPQL과 반환할 엔티티의 클래스 타입인 Member.class를 넘겨주고 getResultList() 메서드를 실행하면 JPA는 JPQL을 SQL로 변환해서 DB를 조회한다. 그리고 조회한 결과로 Member 엔티티를 생성해서 반환한다.
Criteria 쿼리 소개
Criteria의 장점은 문자가 아닌query.select(m).where(...)처럼 코드로 JPQL을 작성할수 있다는 점이다.
JPQ은 오타가 발생하면 컴파일은 성공하고 애플리케이션 서버에 배포할 수는 있지만 문제는 해당 쿼리가 실행되는 런타임 시점에 오류가 발생한다는 점이다. 이것은 문자기반 쿼리의 단점이다.
반면 Criteria는 문자가 아닌 코드로 작성하기 때문에 컴파일 시점에 오류를 발견할 수 있다.
- Criteria의 장점
- 컴파일 시점에 오류를 발견할 수 있다.
- IDE를 사용하면 코드 자동완성을 지원한다.
- 동적 쿼리를 작성하기 편하다.
이것을 Criteria로 작성해보자.
아쉬운 점은 m.get("username")을 보면 필드 명을 문자로 작성했다. 만약 이 부분도 문자가 아닌 코드로 작성하고 싶으면 메타 모델을 사용하면 된다.
- 메타 모델 API
JPA는 자바가 제공하는 어노테이션 프로세서 기능을 사용하면 어노테이션을 분석해서 클래스를 생성하는데, 이 기능을 사용하여 Member엔티티 클래스로부터 Member_ 라는 Criteria 전용 클래스를 생성하는데 이것을 메타 모델이라 한다.
메타 모델을 사용하면 온전히 코드만 사용해서 쿼리를 작성할 수 있다.
username 에서 Member_.username로 변경되었다. Criteria가 가진 장점이 많지만 모든 장점을 상쇄할 정도로 복잡하고 장황하다. 따라서 사용하기 불편한 건 물론이고 Criteria로 작성한 코드도 한눈에 들어오지 않는다는 단점이 있다.
QueryDSL 소개
QueryDSL도 Criteria처럼 JPQL 빌더 역할을 한다. 장점은 코드 기반이면서 단순하고 사용하기 쉽다. 그래서 QueryDSL은 Criteria보다 사용하기 쉽다.
QMember는 Member 엔티티 클래스를 기반으로 생성된 QueryDSL 쿼리 전용 클래스이다.
네이티브 SQL 소개
JPA는 SQL을 직접 사용할 수 있는 기능을 지원하는데 이것을 네이티브 SQL이라 한다. JPQL을 사용해도 가끔은 특정 DB에 의존하는 기능을 사용해야 할 때가 있다. ex) 오라클 CONNECT BY, 특정 DB에서만 동작하는 SQL 힌트같은 것
그리고 SQL은 지원하지만 JPQL이 지원하지 않는 기능도 있다. 하지만 네이티브 SQL의 단점은 특정 DB에 의존하는 SQL을 작성해야 한다는 것이다. 따라서 DB를 변경하면 네이티브 SQL도 변경해야 한다.
JDBC 직접 사용, 마이바티스 같은 SQL 매퍼 프레임워크 사용
이런 일은 JPA를 사용하므로 드물다. 만약 JDBC 커넥션에 직접 접근하고 싶으면 JPA는 JDBC 커넥션을 획득하는 API를 제공하지 않으므로 JPA 구현체가 제공하는 방법을 사용해야 한다.
JDBC나 마이바티스를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다. JDBC, 마이바티스 같은 SQL 매퍼 모두 JPA를 우회해서 DB에 접근한다. 문제는 JPA를 우회하는 SQL에 대해서는 JPA가 전혀 인식하지 못한다. 이런 이슈를 해결하는 방법은 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 DB와 영속성 컨텍스트를 동기화하면 된다.
**참고
스프링 프레임워크를 사용하면 JPA와 마이바티스를 손쉽게 통합할 수 있다. 또한 스프링 프레임워크의 AOP를 적절히 활용하여 JPA를 우회하여 DB에 접근하는 메서드를 호출할 때마다 영속성 컨텍스트를 플러시하면 위에서 언급한 문제도 깔끔하게 해결할 수 있다.
2. JPQL
- JPQL 특징
- 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
- 특정 SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환된다.
Address는 임베디드 타입인데 이것은 값 타입으므로 UML에서 스테레오 타입을 사용해 <<Value>>로 정의했다. 이것은 ERD를 보면 ORDERS 테이블에 포함되어 있다.
기본 문법과 쿼리 API
엔티티를 저장할 때는 EntitiyManager.persist() 메서드를 사용하면 되므로 INSERT문은 없다.(SELECT,UPDATE,DELETE문만 존재)
SELECT 문
- 특징
- 대소문자 구별 : 엔티티와 속성은 대소문자를 구분한다. Member, username 대소문자 구분, SELECT,FROM은 대소문자 구분하지 않는다.
- 엔티티 이름 : JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다. 엔티티 명은 @Entity(name = "XXX")로 지정할 수 있다. 엔티티 명을 지정하지 않으면 클래스 명을 기본값으로 사용한다. 기본값인 클래스 명을 엔티티 명으로 사용하는 것을 추천한다.
- 별칭은 필수 : Member를 m이라는 별칭을 주었다. 따라서 다음 코드처럼 별칭 없이 작성하면 잘못된 문법이라는 오류가 발생한다.
TyperQuery, Query
JPQL을 실행하려면 쿼리 객체를 만들어야 한다. 쿼리 객체는 TypeQuery, Query가 있는데 반환할 타입을 명확하게 지정 가능하면 TypeQuery, 객체를 사용하고, 반환 타입을 명확히 지정할 수 없으면 Query객체를 사용하면 된다.
조회 대상이 Member 엔티티이므로 조회 대상 타입이 명확하다. 이때는 TypeQuery 사용 가능하다.
조회 대상이 String 타입 회원 이름과 Integer 타입인 나이이므로 조회 대상 타입이 명확하지 않다. 이처럼 SELECT 절에서 여러 엔티티나 컬럼을 선택할 때는 반환할 타입이 명확하지 않으므로 Query 객체를 사용해야 한다. Query 객체는 SELECT 절의 조회 대상이 하나면 Object를 반환 ,둘 이상이면 Object[]를 반환한다.
결고 조회
- query.getResultList() : 결과를 예제로 반환한다. 만약 결과가 없으면 빈 컬렉션을 반환한다.
- query.getSingleResult() : 결과가 정확히 하나일 때 사용한다.
-결과가 없으면 javax.persistence.NoResultException 예외가 발생한다.
-결과가 1개보다 많으면 javax.persistence.NonUniqueResultException 예외가 발생한다.
파라미터 바인딩
JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩도 지원한다.
- 이름 기준 파라미터 : 앞에 : 를 사용한다.
:username 이라는 이름 기준 파라미터를 정의하고 query.setParameter()에서 username이라는 이름으로 파라미터를 바인딩한다.
- 위치 기준 파라미터 : ? 다음에 위치 값을 주면 된다.
위치 기준 파라미터 방식보다 이름 기준 파라미터 바인딩 방식이 더 명확하다.
프로젝션
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 한다. 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 있다.
- 엔티티 프로젝션
둘다 엔티티 프로젝션 대상으로 사용했다. 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
- 임베디드 타입 프로젝션
임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다. 따라서 다음은 Address를 조회의 시작점으로 사용해서 잘못된 쿼리이다.
임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.
- 스칼라 타입 프로젝션
숫자, 문자, 날짜와 같은 기본 데이터 타입들을 사용한 쿼리
- 여러 값 조회
꼭 필요한 데이터들만 선택해서 조회하는 것이다. 프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신에 Query를 사용해야 한다.
제네릭에 Object[]를 사용하여 간결하게 개발 할 수 있다.
스칼라 타입 뿐만 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있다.
이때도 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
- NEW 명령어
위 예시는 username, age 두 필드를 프로젝션해서 타입을 지정할 수 없으므로 TypeQuery 사용 불가하다. 따라서 Object[]를 반환받았다. 실제 개발시에는 Object[]를 사용하지 않고 UserDTO처럼 의미 있는 객체로 변환해서 사용할 것이다.
이런 객체 변환 작업은 지루 하므로 NEW 명령어를 사용 해보자.
SELECT 다음 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있는데 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다. 그리고 NEW 명령어를 사용한 클래스로 TypeQuery를 사용할 수 있어서 지루한 객체 변환 작업을 줄일 수 있다.
- New 명령어 사용 시 주의사항 2가지
- 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
- 순서와 타입이 일치하는 생성자가 필요하다.
페이징 API
페이징 처리용 SQL은 지루하고 반복적이다. 더 큰 문제는 DB마다 페이징을 처리하는 SQL문법이 다르다.
JPA는 페이징을 두 API로 추상화하였다.
- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작한다)
- setMaxResult(int maxResult) : 조회할 데이터 수
FirstResult 시작은 0이므로 11번째부터 시작해서 20건의 데이터를 조회한다. 따라서 11~30번 데이터를 조회한다.
DB마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 DB방언 덕분이다.
DB마다 SQL이 다르고 오라클과 SQLServer는 페이징 쿼리를 따로 공부해야 SQL을 작성할 수 있을 정도로 복잡하다. 참고로 ? 에 바인딩하는 값도 DB마다 다른데 이 값도 적절한 값을 입력한다. 페이징 SQL을 더 최적화하고 싶다면 JPA가 제공하는 페이징 API가 아닌 네이티브 SQL을 직접 사용해야 한다.
집합과 정렬
집합은 집합함수와 함께 통계 정보를 구할 때 사용한다.
- 집합 함수
- 집합 함수 사용 시 참고사항
- NULL 값은 무시하므로 통계에 잡히지 않는다 (DISTINCT 가 정의되어 있어도 무시된다.)
- 만약 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL 값이 된다 단 COUNT는 0이 된다.
- DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다.
ex) select COUNT(DISTINCT m.age) from Member m - DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.
- GROUP BY, HAVING
GROUP BY는 특정 그룹끼리 묶어준다.
HAVING은 GROUP BY로 그룹화한 통계 데이터를 기준을 필터링해준다.
문법은 다음과 같다.
보통 통계 쿼리는 전체 데이터를 기준으로 하기 때문에 실시간으로 사용하기엔 부담이 된다. 그래서 결과가 아주 많다면 통계 결과를 저장하는 테이블을 별도로 만들어 두고 사용자가 적은 새벽에 통계 쿼리를 실행해서 그 결과를 보관하는 것이 좋다.
- ORDER BY
ORDER BY는 결과를 정렬할 때 사용한다.
- ASC : 오름차순(기본값)
- DESC : 내림차순
문법에서 이야기하는 상태필드는 t.name 같이 객체의 상태를 나타내는 필드를 말한다. 그리고 결과 변수는 SELECT 절에 나타나는 값을 말한다.다음 예에서 cnt가 결과 변수다.
JPQL 조인
- 내부 조인
JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다. 여기서 m.team이 연관 필드인데 연관 필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말한다.
- FROM Member m : 회원을 선택하고 m이라는 별칭을 주었다.
- Member m JOIN m.team t : 회원이 가지고 있는 연관 필드로 팀과 조인한다. 조인한 팀에는 t라는 별칭을 주었다.
쿼리는 '팀A' 소속인 회원을 나이 내림차순으로 정렬하고 회원명과 팀명을 조회한다.
만약 조인한 두 개의 엔티티를 조회하려면 다음과 같이 JPQL을 작성하면 된다.
서로 다른 타입의 두 엔티티를 조회했으므로 TypeQuery를 사용할 수 없다. 따라서 다음처럼 조회해야 한다.
- 외부 조인
- 컬렉션 조인
일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.
- [회원 -> 팀]으로의 조인은 다대일 조인이면서 단일 값 연관 필드(m.team)를 사용한다.
- [팀 -> 회원]으로의 조인은 일대 다 조인이면서 컬렉션 값 연관 필드(m.members)를 사용한다.
이것은 팀과 팀이 보유한 회원목록을 컬렉션 값 연관 필드로 외부 조인했다.
- 세타 조인
WHERE 절을 사용해서 세타 조인을 할 수 있다. 참고로 세타 조인은 내부 조인만 지원한다.
위를 보면 전혀 관련없는 Member.username과 Team.name을 조인한다.
- JOIN ON절
조인할 때 ON 절을 지원한다. ON절을 사용하면 조인 대상을 필터링하고 조인할 수 있다. 참고로 내부 조인의 ON절은 WHERE 절을 사용할 때와 결과가 같으므로 보통 ON 절은 외부 조인에서만 사용한다.
모든 회원을 조회하면서 회원과 연관된 팀도 조회하자. 이대 팀은 이름이 A인 팀만 조회한다.
SQL 결과를 보면 and t.name = 'A'로 조인 시점에 조인 대상을 필터링한다.
페치 조인
페치 조인은 SQL에서 이야기 하지는 않고 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 이것은 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능인데 join fetch 명령어로 사용할 수 있다.
- 엔티티 페치 조인
페치 조인을 사용해서 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회하는 JPQL
이렇게 하면 연관된 엔티티나 컬렉션을 함께 조회하는데 여기서는 회원과 팀을 함께 조회한다. 참고로 JPQL 조인과는 다르게 m.team 다음에 별칭이 없는데 페치 조인은 별칭을 사용할 수 없다.(하이버네이트는 페치 조인에도 별칭을 허용한다)
엔티티 페치 조인 JPQL에서 select m 으로 회원 엔티티만 선택했는데 실행된 SQL은 회원과 연관된 팀도 함께 조회된 것을 확인할 수 있다. 그리고 위 조인 결과 객체를 보면 회원과 팀 객체가 객체 그래프를 유지하면서 조회된 것을 확인할 수 있다.
이것을 다시 말하자면
출력 결과는 다음과 같다.
회원과 팀을 지연 로딩으로 설정했다고 가정해보자. 회원을 조회할 때 페치 조인을 사용해서 팀도 함께 조회했으므로 연관된 팀 엔티티는 프록시가 아닌 실제 엔티티다. 따라서 연관된 팀을 사용해도 지연 로딩이 일어나지 않는다. 그리고 프록시가 아닌 실제 엔티티이므로 회원 엔티티가 영속성 컨텍스트에서 분리되어 준 영속 상태가 되어도 연관된 팀을 조회할 수 있다.
- 컬렉션 페치 조인
이번에는 일대다 관계인 컬렉션을 페치 조인해보자.
일대다 조인은 결과가 증가할 수 있지만 일대일, 대대일 조인은 결과가 증가하지 않는다.
- 페치 조인과 DISTINCT
JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 건 물론이고 애플리케이션에서 한번 더 중복을 제거한다. 바로 직전 컬렉션 페치 조인은 팀 A가 중복으로 조회된다. 다음처럼 DISTINCT를 추가해보자.
하지만 지금은 각 로우의 데이터가 다르므로 DISTINCT는 효과가 없다.
다음으로 애플리케이션에서 distinct 명령어를 보고 중복된 데이터를 걸러낸다. select distinct t 의미는 팀 엔티티의 중복을 제거하라는 것이다. 따라서 중복인 팀A는 하나만 조회된다.
- 페치 조인과 일반 조인의 차이
실행된 SQL을 보면 팀만 조회하고 조인했던 회원은 전혀 조회되지 않는다. JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다. 따라서 팀 엔티티만 조회하고 연관된 회원 컬렉션은 조회하지 않는다. 만약 회원 컬렉션을 지연 로딩으로 설정하면 밑의 그림처럼 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환한다. 즉시 로딩으로 설정하면 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한 번 더 실행한다.
반면 페치 조인을 사용하면 연관된 엔티티도 함께 조회한다.
- 페치 조인의 특징과 한계
페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화 할 수 있다. 페치 조인은 글로벌 로딩 전략(엔티티에 직접 적용하는 로딩 전략 즉, 애플리케이션 전체에 영향을 미치는 전략)보다 우선시 한다. 예를 들어 글로벌 로딩 전략을 지연 로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치 조인을 적용해서 함께 조회한다.
최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에서 항상 즉시 로딩이 일어난다. 물론 일부는 빠를 수는 있지만 전체로 보면 사용하지 않는 엔티티를 자주 로딩하므로 오히려 성능에 악영향을 미칠 수 있다. 따라서 글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다.
또한 페치 조인을 사용하면 연관된 엔티티를 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않는다. 따라서 준영속 상태에서도 객체 그래프를 탐색할 수 있다.
- 페치 조인의 한계
- 페치 조인 대상에는 별칭을 줄 수 없다.(하이버네이트를 포함한 몇몇은 가능 하지만 무결성이 깨질 수 있으므로 조심히 사용해야 한다.)
- 둘 이상의 컬렉션을 페치할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
페치 조인은 SQL 한 번으로 여러 엔티티를 조회할 수 있어서 성능 최적화에 상당히 유용하다. 하지만 모든 것을 페치 조인으로 해결할 수 없고, 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 억지로 페치 조인을 사용하기 보다 필요한 필드들만 조회해서 DTO로 반환하는 것이 더 효과적일 수 있다.
경로 표현식
경로 표현식은 쉽게 말해 .(점)을 찍어 객체 그래프를 탐색하는 것이다.
여기서 m.username, m.team, m.orders, t.name 모두 경로 표현식을 사용한다.
- 경로 표현식의 용어 정리
- 상태 필드 : 단순히 값을 저장하기 위한 필드(필드 or 프로퍼티)
- 연관 필드 : 연관관계를 위한 필드, 임베디드 타입 포함(필드 or 프로퍼티)
- 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티
- 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션
- 상태 필드 : ex) t.username, t.age
- 단일 값 연관 필드 : ex) m.team
- 컬렉션 값 연관 필드 : ex) m.orders
- 경로 표현식과 특징
- 상태 필드 경로 : 경로 탐색의 끝이다. 더는 탐색할 수 없다.
- 단일 값 연관 경로 : 묵시적으로 내부 조인이 일어난다. 단일 값 연관 경로는 계속 탐색할 수 있다.
- 컬렉션 값 연관 경로 : 묵시적으로 내부 조인이 일어난다. 더는 탐색할 수 없다. 단 FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있다.
위를 보면 총 3번의 조인이 발생했다. 참고로 o.address처럼 임베디드 타입에 접근하는 것도 단일 값 연관 경로 탐색이지만 주문 테이블에 이미 포함되어 있으므로 조인이 발생하지 않는다.
- 컬렉션 값 연관 경로 탐색
JPQL을 다루면서 가장 많이 하는 실수 중 하나는 컬렉션 값에서 경로 탐색을 시도하는 것이다.
t.members처럼 컬렉션까지는 경로 탐색이 가능하지만 그밑의 코드는 허락하지 않는다. 만약 하고 싶다면 다음 코드처럼 조인을 사용해서 새로운 별칭을 획득해야 한다.
또한 size를 사용하면 COUNT 함수를 사용하는 SQL로 적절히 변환된다.
- 경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인이다.
- 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 한다.
- 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM 절에 영향을 준다.
되도록 묵시적 조인보다는 명시적 조인을 사용하자.(한눈에 파악하기 어렵기 때문에)
서브 쿼리
JPQL도 서브 쿼리를 지원하지만 WHERE, HAVING 절에서만 사용 가능하고, SELECT, FROM 절에서는 사용할 수 없다.
- 서브 쿼리 함수
- [NOT] EXISTS(subquery)
- {ALL | ANY | SOME} (subquery)
- [NOT] IN (subquery)
- EXISTS
서브쿼리에 결과가 존재하면 참이다. NOT은 반대
- { ALL | ANY | SOME }
비교 연산자와 같이 사용한다. { = | > | >= | < | <= | <>}
ALL : 조건을 모두 만족하면 참이다.
ANY 혹은 SOME : 둘다 같은 의미다. 조건을 하나라도 만족하면 참이다.
- IN
서브쿼리의 결과 중 하나라도 같은 것이 있으면 참이다. 참고로 IN은 서브쿼리가 아닌 곳에서도 사용한다.
조건식
- 타입 표현
- 연산자 우선 순위
- 경로 탐색 연산 (.)
- 수학 연산 : +, -(단항 연산자), *, /, +. -
- 비교 연산 : =, > , >=, <, <=, <>(다름), [NOT] BETWEEN, [NOT] LIKE, [NOT]IN, IS[NOT] NULL, IS[NOT] EMPTY, [NOT] MEMBER [OF], [NOT] EXISTS
- 논리 연산 : NOT, AND, OR
- 논리 연산과 비교식
- 논리 연산
AND : 둘다 만족해야 함
OR : 둘중 하나만 만족해도 참
NOT : 조건식의 결과 반대 - 비교식
- Between, IN, Like, NULL 비교
Between
문법 : X [NOT] BETWEEN A AND B
설명 : X 는 A~B 사이의 값이면 참
IN
문법 :X [NOT] IN(예제)
설명 : X와 같은 값이 예제에 하나라도 있으면 참이다. IN 식의 예제에는 서브쿼리 사용할 수 있다.
Like
문법 : 문자표현식 [NOT] LIKE 패턴값 [ESCAPE]
설명 : 문자표현식과 패턴값을 비교한다.
- % : 아무 값들이 입력되어도 된다 (값이 없어도 됨).
- _ : 한 글자는 아무 값이 입력되어도 되지만 값이 있어야 한다.
NULL
문법 : { 단일값 경로 | 입력 파라미터 } IS [NOT] NULL
설명 : NULL 인지 비교한다. NULL은 = 으로 비교하면 안 되고 꼭 IS NULL을 사용 해야 한다.
- 컬렉션 식
컬렉션 식은 컬렉션에만 사용하는 특별한 기능이다. 참고로 컬렉션은 컬렉션 식 이외에 다른 식은 사용할 수 없다.
빈 컬렉션 비교 식
문법 : {컬렉션 값 연관 경로} IS [NOT] EMPTY
설명 : 컬렉션에 값이 비었으면 참
컬렉션의 멤버 식
문법 : {엔티티나 값} [NOT] MEMBER [OF] (컬렉션 값 연관 경로}
설명 : 엔티티나 값이 컬렉션에 포함되어 있으면 참
- 스칼라 식
스칼라는 숫자, 문자, 날짜, case, 엔티티 타입 같은 가장 기본적인 타입들을 말한다.
- 수학 식
+, - : 단항 연산자
*, /, +,- : 사칙연산 - 문자함수
- 수학함수
- 날짜함수
CURRENT_DATE : 현재 날짜
CURRENT_TIME : 현재 시간
CURRENT_TIMESTAMP : 현재 날짜 시간
DB들은 각자의 방식으로 더 많은 날짜 함수를 지원한다. 그리고 각각의 날짜 함수는 하이버네이트가 제공하는 DB 방언에 등록되어 있다. 물론 다른 DB를 사용하면 동작하지 않는다.
- CASE 식
- 기본 CASE
- 심플 CASE
- COALECE
- NULLIF
기본 CASE
심플 CASE : 심플 CASE는 조건식을 사용할 수 없지만, 문법이 단순하다. 참고로 자바의 switch case문과 비슷하다.
COALESCE : 스칼라식을 차례대로 조회해서 null이 아니면 반환한다.
NULLIF : 두 값이 같으면 null을 반환하고 다르면 첫 번째 값을 반환한다. 집합 함수는 null을 포함하지 않으므로 보통 집합 함수와 함께 사용한다.
다형성 쿼리
JPQL로 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회한다.
아래와 같이 조회하면 Item의 자식도 함께 조회한다.
단일 테이블 전략(InheritanceType.SINGLE_TABLE)을 사용할 때 실행되는 SQL은 다음과 같다.
조인 전략(InheritanceType.JOINED)을 사용할 때 실행되는 SQL은 다음과 같다.
- TYPE
TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용한다.
- TREAT
자바의 타입 캐스팅과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. JPA 표준은 FROM, WHERE절에서, 하이버네이트는 SELECT 절에서도 TREAT를 사용할 수 있다.
JPQL을 보면 treat를 사용해서 부모 타입인 Item을 자식 타입인 Book으로 다룬다. 따라서 author 필드에 접근할 수 있다.
사용자 정의 함수 호출
JPA 2.1부터 사용자 정의 함수를 지원한다.
하이버네이트 구현체를 사용하면 밑과 같이 방언 클래스를 상속해서 구현하고 사용할 DB 함수를 미리 등록해야 한다.
그리고 밑과 같이 hibernate.dialect에 해당 방언을 등록해야 한다.
하이버네이트 구현체를 사용하면 다음과 같이 축약해서 사용할 수 있다.
기타 정리
- enum은 = 비교 연산만 지원한다.
- 임베디드 타입은 비교를 지원하지 않는다.
- EMPTY STRING
JPA 표준은 ' 와 ' 을 길이 0인 Empty String으로 정했지만 DB에 따라 NULL로 사용하는 DB도 있으므로 확인하고 사용해야 한다.
- null 정의
- 조건을 만족하는 데이터가 하나도 없으면 NULL이다.
- NULL은 알 수 없는 값이다. NULL과이 모든 수학적 계산 결과는 NULL이 된다.
- NULL == NULL 은 알 수 없는 값이다.
- NULL is NULL은 참이다.
엔티티 직접 사용
- 기본 키 값
객체 인스턴스는 참조 값으로 식별, 테이블 로우는 기본 키 값으로 식별한다. 따라서 JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
두 번째의 count(m)을 보면 엔티티의 별칭을 직접 넘겨주었다. 이렇게 엔티티를 직접 사용하면 JPQL이 SQL로 변환될 때 해당 엔티티의 기본 키를 사용한다. 따라서 다음 실제 실행된 SQL은 둘 다 같다.
엔티티를 파라미터로 직접 받아보자
JPQL과 SQL을 비교해보면 JPQL에서 where m = :member로 엔티티를 직접 사용하는 부분이 SQL에서 where m.id = ? 로 기본 키 값을 사용하도록 변환된 것을 확인할 수 있다.
- 외래 키 값
위에서 m.team.id 를 보면 Member와 와 Team 간에 묵시적 조인이 일어날 것 같지만 MEMBER 테이블이 team_id 외래 키를 가지고 있으므로 묵시적 조인은 일어나지 않는다.
Named 쿼리 : 정적 쿼리
- 동적 쿼리 : em.createQuery("select .. ")처럼 JQPL을 문자로 완성해서 직접 넘기는 것을 동적 쿠리라 한다.
- 정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을 Named 쿼리라 한다.
Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해 둔다. 따라서 오류를 빨리 확인할 수 있고, 사용하는 시점에서 파싱된 결과를 재사용하므로 성능상 이점도 있다. 그리고 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 성능 최적화에도 도움이 된다.
- Named 쿼리를 어노테이션에 정의
@NamedQuery.name에 쿼리 이름을 부여하고 @NamedQuery.query에 사용할 쿼리를 입력한다.
사용은 em.createNamedQuery() 메서드에 Named 쿼리 이름을 입력하면 된다.
두 개 이상의 Named 쿼리를 정의하려면 @NamedQueries 를 사용하자.
- lockMode : 쿼리 실행 시 락을 건다.
- hints : 여기서 힌트는 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트다. 예시로 2차 캐시를 다룰 때 사용한다.
- Named 쿼리를 xml에 정의
JPA에서 어노테이션으로 작성할 수 있는 것은 XML로도 작성할 수 있다. 하지만 Named 쿼리를 작성할 때는 XML을 사용하는 것이 더 편하다.
그리고 정의한 ormMember.xml을 인식하도록 META-INF/persistence.xml에 다음 코드를 추가해야 한다.
**참고 : META-INF/orm.xml은 JPA가 기본 매핑파일로 인식해서 별도 설정을 안해도 되지만 위에는 매핑 파일 이름이 ormMember.xml이므로 persistence.xml에 정보를 추가했다.
- 환경에 따른 설정
만약 XML과 어노테이션에 같은 설정이 있으면 XML이 우선권을 가진다.
'Spring > JPA' 카테고리의 다른 글
객체지향 쿼리 언어(3) QueryDSL (0) | 2023.07.17 |
---|---|
객체지향 쿼리 언어(2) Criteria (0) | 2023.07.17 |
값 타입 (0) | 2023.07.16 |
프록시와 연관관계 관리 (0) | 2023.07.15 |
고급 매핑 (0) | 2023.07.14 |