Criteria
Criteria 쿼리는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 API
- 문자가 아닌 코드로 JPQL을 작성하므로 문법 오류를 컴파일 단계에서 잡을 수 있다.
- 문자 기반의 JPQL보다 동적 쿼리를 안전하게 생성할 수 있다.
- 코드가 복잡하고 장황해서 직관적으로 이해가 힘들다는 단점도 있다.
//JPQL: select m from Member m
//Criteria 쿼리 빌더
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
//Criteria 생성, 반환 타입 지정
CriteriaQuery<Member> criteriaQuery = criteriaBuilder.createQuery(Member.class);
//from 절
Root<Member> member = criteriaQuery.from(Member.class);
//select 절
criteriaQuery.select(member);
TypedQuery<Member> query = entityManager.createQuery(criteriaQuery);
List<Member> memberList = query.getResultList();
- criteria 쿼리를 생성하려면 먼저 Criteria 빌더를 얻어야 한다. -> EntityManager나 EntityManagerFactory에서 얻을 수 있다.
- Criteria 쿼리 빌더에서 Criteria 쿼리를 생성한다. -> 반환 타입을 지정할 수 있다.
- from 절을 생성한다. 반환된 값 member는 Criteria에서 사용하는 특별한 별칭이다. -> member를 조회의 시작점이라는 의미로 쿼리 루트(Root)라 한다.
- select 절을 생성한다.
다른 예제로 검색 조건(where)와 정렬(Order by)를 다뤄보자.
//JPQL:
//select m from Member m
//where m.username='회원1' and m.age > 10
//order by m.age desc
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Member> criteriaQuery = criteriaBuilder.createQuery(Member.class);
Root<Member> member = criteriaQuery.from(Member.class);
//where 절 정의
Predicate usernameEqual = criteriaBuilder.equal(member.get("username"), "회원1");
Predicate ageGt = criteriaBuilder.greaterThan(member.<Integer>get("age"), 10);
Predicate predicate = criteriaBuilder.and(usernameEqual, ageGt);
//정렬 조건 정의
Order ageDesc = criteriaBuilder.desc(member.get("age"));
criteriaQuery.select(member)
.where(predicate)
.orderBy(ageDesc);
TypedQuery<Member> query = entityManager.createQuery(criteriaQuery);
List<Member> memberList = query.getResultList();
where 절을 두 개 이상 하려면 Criteria 빌더에서 and()를 선언해서 하면된다.
greaterThan(member.<Integer>get("age"), 10)은 "age"의 타입 정보를 알지 못하므로 제네릭으로 반환 타입 정보를 명시해주어야한다.
greaterThan() 대신에 gt()를 써도 된다.
쿼리 루트(Query Root)와 별칭
- Root<Member> member = criteriaQuery.from(Member.class) 여기서 member가 쿼리 루트이다.
- 쿼리 루트는 조회의 시작점이다.
- Criteria에서 사용되는 별칭이다. = JPQL의 별칭이라 생각하면 된다.
- 별칭은 엔티티에만 부여할 수 있다.
- member.get("username")은 JPQL의 m.username과 같다.
CriteriaBuilder 인터페이스
public interface CriteriaBuilder {
//조회값 반환 타입 : Object
CriteriaQuery<Object> createQuery();
//조회값 반환 타입 : 엔티티, 임베디드 타입, 기타
<T> CriteriaQuery<T> createQuery(Class<T> resultClass);
//조회값 반환 타입 : Tuple
CriteriaQuery<Tuple> createTupleQuery();
...
}
CriteriaQuery 인터페이스
public interface CriteriaQuery<T> extends AbstractQuery<T> {
//한 건 지정
CriteriaQuery<T> select(Selection<? extends T> selection);
//여러 건 지정
CriteriaQuery<T> multiselect(Selection<?>... selections);
//여러 건 지정
CriteriaQuery<T> multiselect(List<Selection<?>> selectionList);
}
QueryDSL
쿼리를 문자가 아닌 코드로 작성해도, 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는 프로젝트가 QueryDSL 이다.
검색 조건 쿼리
public void queryDSL(EntityManager entityManager) {
JPAQuery query = new JPAQuery(entityManager);
QMemberEntity member = QMemberEntity.memberEntity;
List<MemberEntity> memberEntityList = query.from(member)
.where(member.username.eq("회원1"))
.orderBy(member.username.desc())
.list(member);
}
실행된 JPQL
select member
from MemberEntity member
where member.username = ?1
order by member.username desc
페이징과 정렬
offset과 limit을 사용한 페이징
public void queryDSL(EntityManager entityManager) {
JPAQuery query = new JPAQuery(entityManager);
QMemberEntity member = QMemberEntity.memberEntity;
List<MemberEntity> memberEntityList = query.from(member)
.where(member.username.eq("회원1"))
.orderBy(member.username.desc())
.offset(10)
.limit(20)
.list(member);
}
QueryModifiers를 사용한 페이징
public void queryDSL(EntityManager entityManager) {
JPAQuery query = new JPAQuery(entityManager);
QMemberEntity member = QMemberEntity.memberEntity;
QueryModifiers queryModifiers = new QueryModifiers(20L, 10L); //limit, offset
List<MemberEntity> memberEntityList = query.from(member)
.where(member.username.eq("회원1"))
.orderBy(member.username.desc())
.restrict(queryModifiers)
.list(member);
}
페이징과 정렬 ListResults()사용
public void queryDSL(EntityManager entityManager) {
JPAQuery query = new JPAQuery(entityManager);
QMemberEntity member = QMemberEntity.memberEntity;
SearchResults<MemberEntity> result = query.from(member)
.where(member.username.eq("회원1"))
.orderBy(member.username.desc())
.offset(10)
.limit(20)
.listResults(member);
long total = result.getTotal(); //검색된 데이터 전체 수
long limit = result.getLimit();
long offset = result.getOffset();
List<Member> results = result.getResults(); //조회된 데이터
}
그룹
public void queryDSL(EntityManager entityManager) {
JPAQuery query = new JPAQuery(entityManager);
QMemberEntity member = QMemberEntity.memberEntity;
List<MemberEntity> memberEntityList = query.from(member)
.groupBy(member.age)
.having(member.age.gt(20))
.list(member);
}
조인
기본 조인
public void queryDSL(EntityManager entityManager) {
JPAQuery query = new JPAQuery(entityManager);
QMemberEntity member = QMemberEntity.memberEntity;
QOrderEntity order = QOrderEntity.orderEntity;
QOrderItemEntity orderItem = QOrderItemEntity.orderItemEntity;
query.from(order)
.join(order.memberEntity, member)
.leftJoin(order.orderItems, orderItem)
.list(member);
}
조인 on 사용
public void queryDSL(EntityManager entityManager) {
JPAQuery query = new JPAQuery(entityManager);
QMemberEntity member = QMemberEntity.memberEntity;
QOrderEntity order = QOrderEntity.orderEntity;
QOrderItemEntity orderItem = QOrderItemEntity.orderItemEntity;
query.from(order)
.leftJoin(order.orderItems, orderItem)
.on(orderItem.count.gt(2))
.list(member);
}
페치 조인 사용
public void queryDSL(EntityManager entityManager) {
JPAQuery query = new JPAQuery(entityManager);
QMemberEntity member = QMemberEntity.memberEntity;
QOrderEntity order = QOrderEntity.orderEntity;
QOrderItemEntity orderItem = QOrderItemEntity.orderItemEntity;
query.from(order)
.join(order.memberEntity, member).fetch()
.leftJoin(order.orderItems, orderItem).fetch()
.list(member);
}
from 절에 여러 조건 사용
public void queryDSL(EntityManager entityManager) {
JPAQuery query = new JPAQuery(entityManager);
QMemberEntity member = QMemberEntity.memberEntity;
QOrderEntity order = QOrderEntity.orderEntity;
QOrderItemEntity orderItem = QOrderItemEntity.orderItemEntity;
query.from(order, member)
.where(order.memberEntity.eq(member))
.list(member);
}
서브 쿼리
서브 쿼리의 결과가 하나면 unique()
QItem item = QItem.item;
QItem itemSub = new QItem("itemSub");
query.from(item)
.where(item.price.eq(
new JPASubQuery().from(itemSub).unique(itemSub.price.max())
))
.list(item);
여러 건이면 list()
QItem item = QItem.item;
QItem itemSub = new QItem("itemSub");
query.from(item)
.where(item.in(
new JPASubQuery().from(itemSub)
. where (item. name. eq (itemSub. name))
.list(itemSub)
))
.list(item);
프로젝션과 결과 반환
select 절에 조회 대상을 지정하는 것을 프로젝션이라 한다.
Tuple이라는 Map과 비슷한 내부 타입을 사용한다.
쿼리 결과를 엔티티가 아닌 특정 객체로 받고 싶을 때 빈 생성을 사용한다.
- 프로퍼티 접근
- 필드 직접 접근
- 생성자 사용
프로퍼티 접근
setter를 이용해서 값을 채운다.
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.bean(ItemDTO.class, item.name.as("username"), item.price));
필드 직접 접근
필드에 직접 접근해서 채운다. -> private 여도 상관없다.
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.fields(ItemDTO.class, item.name.as("username"), item.price));
생성자 사용
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.constructor(ItemDTO.class, item.name, item.price));
수정, 삭제 배치 쿼리
- JPQL 배치 쿼리와 같이 영속성 컨텍스트를 무시하고 데이터베이스를 직접 쿼리한다는 점에 유의 해야 한다.
- JPAUpdateClause, JPADeleteClause를 사용한다.
동적 쿼리
BooleanBuilder를 사용하면 특정 조건에 따른 동적 쿼리를 편리하게 생성할 수 있다.
SearchParam param = new SearchParam();
param.setName("시골개발자");
param.setPrice(10000);
QItem item = QItem.item;
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(param.getName())) {
builder.and(item.name.contains(param.getName()));
}
if (param.getPrice() != null) {
builder.and(item.price.gt(param.getPrice()));
}
List<Item> result = query.from(item)
.where(builder)
.list(item);
네이티브 SQL
JPQL은 표준 SQL이 지원하는 대부분의 문법과 SQL 함수들을 지원하지만 특정 데이터베이스에 종속적인 기능은 지원하지 않는다.
- 특정 데이터베이스만 지원하는 함수, 문법, SQL 쿼리 힌트
- 인라인 뷰(From 절에서 사용하는 서브쿼리), UNION, INTERSECT
- 스토어드 프로시저
JPA는 특정 데이터베이스에 종속적인 기능을 사용할 수 있는 다양한 방법을 열어두었다.
특정 데이터베이스에 종송적인 기능을 지원하는 방법은 다음과 같다.
- 특정 데이터베이스만 사용하는 함수
- JPQL에서 네이티브 SQL함수를 호출할 수 있다.
- 하이버네이트는 데이터베이스 방언에 각 데이터베이스에 종속적인 함수를 정의하였다. - 특정 데이터베이스만 지원하는 SQL 쿼리 힌트
- 하이버네이트를 포함한 몇몇 JPA 구현체들이 지원한다. - 인라인 뷰, UNION, INTERSECT
- 하이버네이트는 지원하지 않지만 일부 JPA 구현체들이 지원한다. - 스토어 프로시저
- JPQL에서 스토어드 프로시저를 호출할 수 있다. - 특정 데이터베이스만 지원하는 문법
- 오라클의 CONNECT BY 처럼 특정 데이터베이스에 너무 종속적인 SQL문법은 지원하지 않는다. 이때는 네이티브 SQL을 사용해야 한다.
다양한 이유로 JPQL을 사용할 수 없을 때 JPA는 SQL을 직접 사용할 수 있는 기능을 제공하는데 이것을 네이티브 SQL이라 한다.
네이티브 SQL을 사용하면 엔티티를 조회할 수 있고 JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있다.
벌크 연산시 주의점
- 벌크 연산이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리 한다는 점에 주의해야 한다.
- 업데이트 벌크 연산을 한 후에 영속성 컨텍스트를 통해서 조회하게 되면 영속성 컨텍스트에 이미 존재했던 엔티티의 경우 변경되지 않은 값으로 조회가 될 수 있다.
해결 방법
- em.refresh() 사용
- 벌크 연산 먼저 실행 후 조회
- 벌크 연산 수행 후 영속성 컨텍스트 초기화(em.clear())
영속성 컨텍스트와 JPQL
- select m from Member m // 엔티티 조회(관리O)
- select o.address from Order o //임베디드 타입 조회(관리X)
- select m.id, m.username from Member m // 단순 필드 조회 (관리X)
- JPQL로 조회한 엔티티는 영속상태이다.
- 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환한다.
- 같은 식별자를 가진 엔티티는 영속성 컨텍스트에 중복해서 존재할 수 없다
- 영속성 컨텍스트에 있는 엔티티가 수정 중일 수 있으니 조회해서 가져온 것과 대체할 수 없다
- 영속성 컨텍스트에서 조회한 데이터는 동일성을 보장해야 하기에 새로 검색한 엔티티를 버리고 기존 엔티티를 반환한다.
find() vs JPQL
- find()의 경우 영속성 컨텍스트에 존재하면 영속성 컨텍스트의 엔티티를 반환하지만 JPQL은 실제 데이터베이스에서 조회를 한 후에 영속성 컨텍스트에 엔티티가 존재하는지 비교 후 엔티티를 반환한다.
- 따라서 JPQL로 조회하는 경우 SQL이 매번 실행 된다.
JPQL과 플러시 모드
- AUTO
- JPQL 쿼리가 실행되기 전과 커밋이 실행 될 때 플러시가 동작한다.
- Commit
- 트랜잭션 커밋이 실행 될 때 플러시가 동작하므로 성능의 이점을 가져올 수 있으나 사용시 주의 해야 한다.
'Spring JPA' 카테고리의 다른 글
애플리케이션과 영속성 관리 - 16 (0) | 2021.08.08 |
---|---|
스프링 데이터 JPA - 15 (0) | 2021.07.31 |
객체지향 쿼리 언어 - 13 (0) | 2021.07.17 |
값 타입 - 12 (0) | 2021.07.16 |
프록시와 연관관계 관리 - 11 (0) | 2021.07.13 |
댓글