3. Criteria
Criteria 쿼리는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API이다. 이것을 사용하면 문자가 아닌 코드로 JPQL을 작성하므로 문법 오류를 컴파일 단계에서 잡을 수 있고 문자 기반의 JPQL보다 동적 쿼리를 안전하게 생성할 수있다는 장점이 있다. 하지만 실제 Criteria를 사용해보면 복잡해서 직관적으로 이해가 힘들다는 단점이 있다.
Critera는 결국 JPQL의 생성을 돕는 클래스 모음이다.
Criteria 기초
Criteria API는 javax.persistence.criteria 패키지에 있다.
- Criteria 쿼리를 생성하려면 먼저 Criteria 빌더를 얻어야 한다. 이 빌더는 EntityManager나 EntityManagerFactory 에서 얻을 수 있다.
- Criteria 쿼리 빌더에서 Criteria 쿼리를 생성한다. 이때 반환 타입을 지정할 수있다.
- FROM 절을 생성한다. 반환된 m은 Criteria에서 사용하는 특별한 별칭이다. m을 조회의 시작점이라는 의미로 쿼리 루트라 한다.
- SELECT 절을 생성한다.
이렇게 쿼리를 완성하고 다음 순서는 JPQL과 같다. em.createQuery(cq)에 완성된 Critera 쿼리를 넣어주기만 하면 된다.
- m.get("username") 에서 m은 회원 엔티티의 별칭이다. 이것은 JPQL에서 m.username과 같은 표현이다. 그리고 cb.equal(A,B)는 이름 그대로 A = B, 따라서 m.username = '회원1'과 같은 표현이다.
- JPQL의 m.age desc와 같은 표현이다.
- 만들어둔 조건을 where, orderBy에 넣어서 원하는 쿼리를 생성한다.
- 쿼리 루트와 별칭
- Root<Member>m = cq.from(Member.class) : 여기서 m이 쿼리 루트다.
- 쿼리 루트는 조회의 시작점이다.
- Criteria에서 사용하는 특별한 별칭이다.
- 별칭은 엔티티에만 부여할 수 있다.
- 경로 표현식
- m.get("username")는 JPQL의 m.username과 같다.
- m.get("team").get("name")는 JPQL의 m.team.name과 같다.
m.get("age")에서는 "age"의 타입 정보를 알지 못한다. 따라서 제네릭으로 반환 타입 정보를 명시해야 한다.(보통 String 같은 문자 타입은 지정하지 않아도 된다). 참고로 greaterThan() 대신 gt()를 사용해도 된다.
Criteria 쿼리 생성
밑을 보면 Criteria 쿼리를 생성할 때 파라미터로 쿼리 결과에 대한 반환을 지정할 수 있다.
반환 타입을 지정할수 없거나 반환 타입이 둘 이상이면 밑과 같이 타입을 지정하지 않고 Object로 반환받으면 된다.
물론 반환 타입이 둘 이상이면 Object[] 를 사용하는 것이 편리하다.
반환 타입을 튜플로 받고 싶으면 밑과 같이 사용하면 된다.(튜플은 뒤에서 설명하겠습니다.)
조회
- 조회 대상을 한 건, 여러 건 지정
조회 대상을 하나만 지정하려면 다음처럼 작성하면 된다.
여러 건 지정하려면 multiselect를 사용하자.
다음처럼 cb.array를 사용해도 된다.
- DISTINCT
distinct는 select, multiselect 다음 distinct(true)를 사용하면 된다.
- NEW, construct()
JPQL에서 select new 생성자() 구문을 Criteria 에서는 cb.construct( 클래스 타입 , ...) 로 사용한다.
construct()를 실제 사용하는 코드
JPQL에서는 select new jpabook.domain.MemberDTO()처럼 패키지명을 다 적었지만 Criteria는 코드를 직접 다루므로 MemberDTO.class처럼 간략하게 사용 가능하다.
- 튜플
Criteria는 Map과 비슷한 튜플이라는 특별한 반환 객체를 제공한다.
튜플을 사용하려면 cb.createTupleQuery() 또는 cb.createQuery(Tuple.class)로 Critera를 생성한다.
- 튜플은 튜플의 검색 키로 사용할 튜플 전용 별칭을 필수로 할당해야 한다. 별칭은 alias() 메서드를 사용해서 지정할 수 있다.
- 선언해둔 튜플 별칭으로 데이터를 조회할 수 있다.
튜플은 이름 기반으로 순서 기반의 Object[] 보다 안전하고 tuple.getElements() 같은 메서드를 사용하여 현재 튜플의 별칭과 자바 타입도 조회할 수 있다.
여기서 cq.multiselect 와 cq.select(cb.tuple(...))는 둘다 같은 기능을 한다.
집합
- GROUP BY
cq.groupBy(m.get("team").get("name"))은 JPQL에서 gruop by m.team.name과 과 같다.
- HAVING
이 조건에 팀에 가장 나이 어린 사람이 10살을 초과하는 팀을 조회하는 조건을 추가해보자.
having(cb.gt(minAge, 10))은 JPQL에서 having min(m.age) > 10과 같다.
정렬
조인
조인은 join() 메서드와 JoinType 클래스를 사용한다.
루트 쿼리 (m) 에서 바로 m.join("team") 메서드를 사용하여 회원과 팀을 조인했다.그리고 조인한 team에 t라는 별칭을 주고 내부조인을 사용했다. 외부 조인은 JoinType.LEFT로 설정하면 된다.
페치 조인은 fetch(조인대상, JoinType)을 사용한다.
서브 쿼리
- 간단한 서브 쿼리
- 서브 쿼리 생성 부분을 보면 서브 쿼리는 mainQuery.subquery(...)로 생성한다.
- 메인 쿼리 생성 부분을 보면 where(... , subQuery)에서 생성한 서브 쿼리를 사용한다.
- 상호 관련 서브 쿼리
메인 쿼리와 서브 쿼리 간 관련이 있을 때 Criteria 작성하는지 알아보자. 서브 쿼리에서 메인 쿼리의 정보를 사용하려면 메인 쿼리에서 사용한 별칭을 얻어야 한다. 서브 쿼리는 메인 쿼리의 Root나 Join을 통해 생성된 별칭을 받아서 다음과 같이 사용할 수 있다.
밑은 팀 A에 소속된 회원을 찾도록 했다. 물론 서브 쿼리보다는 조인으로 해결하는 것이 더 효과적일 수 있다. 여기서는 상호 관련 서브 쿼리에 초점을 맞추자.
여기서 핵심은 subQuery.correlate(m)이다. correlate(...)메서드를 사용하면 메인 쿼리의 별칭을 서브 쿼리에서 사용할 수 있다.
IN 식
CASE 식
CASE 식은 selectCase() 와 when(), otherwise() 메서드를 사용한다.
파라미터 정의
JPQL에서 :PARAM1처럼 정의했듯 Criteria도 정의할 수 있다.
- cb.parameter(타입,파라미터 이름) 메서드를 사용해서 파라미터를 정의했다.
- setParameter("usernameParam","회원1")을 사용해서 해당 파라미터에 사용할 값을 바인딩했다.
네이티브 함수 호출
네이티브 SQL 함수를 호출하려면 cb,function(...)메서드를 사용하면 된다.
"SUM" 대신 원하는 네이티브 SQL함수를 입력하면 된다.
동적 쿼리
다양한 검색 조건에 따라 실행 시점에 쿼리를 생성하는 것을 동적 쿼리라 한다. 동적 쿼리는 문자 기반인 JPQL보다는 코드 기반인 Criteria로 작성하는 것이 더 긴 편이다.
JPQL로 동적 쿼리를 구성하는 것은 아슬아슬한 줄타기 같다. 이렇게 단순한 동적 쿼리 코드를 개발해도 문자 더하기로 인해, 여러 번 버그를 만날 것이다. 특히 문자 사이에 공백을 입력하지 않아서 age=:ageandusername=:username처럼 되는 것은 다들 한 번씩 겪는 문제고 where와 and의 위치를 구성하는 것도 신경을 써야 한다.
Criteria는 최소한 공백이나 where, and의 위치로 인해 에러가 발생하지 않는다. 이런 장점에도 불구하고 Criteria의 장황하고 복잡함으로 인해, 코드가 읽기 힘들다는 단점은 여전히 남아 있다.
함수 정리
Criteria는 JPQL 빌더 역할을 하므로 JPQL함수를 코드로 지원한다.
예) m.get("username").isNull()
- 조건함수
- 스칼라와 기타 함수
- 집합 함수
- 분기 함수
Criteria 메타 모델 API
Criteria는 코드 기반으로 컴파일 시점에 오류를 발견할 수 있다. 하지만 m.get("age")에서 age는 문자다. 'age'대신 실수로 'ageaaa'이렇게 잘못 적어도 컴파일 시점에 에러를 발견하지 못한다. 따라서 완전한 코드 기반이라 할 수 없다.
이런 부분까지 코드로 작성하려면 메타 모델 API를 사용하면 된다.
이처럼 문자 기반에서 정적인 코드 기반으로 변경된 것을 볼 수있지만 Member_ 클래스가 필요한데 이것을 메타 모델 클래스라 한다.
이런 클래스를 표준(CANONICAL)메타 모델 클래스라 하는데 줄여서 메타 모델이라 한다. Member_ 메타 모델 클래스는 Member 엔티티를 기반으로 만들어야 한다. 코드 자동 생성기가 엔티티 클래스를 기반으로 메타 모델 클래스들을 만들어 준다.
하이버네이트 구현체를 사용하면 코드 생성기는 org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor를 사용하면 된다.
코드 생성기는 모든 엔티티 클래스를 찾아 "엔티티명_.java"모양의 메타 모델 클래스를 생성해준다.
- 코드 생성기 설정
코드 생성기는 보통 메이븐, 엔트, 그래들 같은 빌드 도구를 사용해서 실행한다.
그리고 mvn compile 명령어를 사용하면 target/generated-sources/annotations/ 하위에 매타 모델 클래스들이 생성된다.
'Spring > JPA' 카테고리의 다른 글
객체지향 쿼리 언어(4) 네이티브 SQL (0) | 2023.07.17 |
---|---|
객체지향 쿼리 언어(3) QueryDSL (0) | 2023.07.17 |
객체지향 쿼리 언어 (1) 객체지향 쿼리 소개, JPQL (0) | 2023.07.16 |
값 타입 (0) | 2023.07.16 |
프록시와 연관관계 관리 (0) | 2023.07.15 |