본문 바로가기
Spring JPA

객체 지향 쿼리 언어 - 14

by 홍굴이 2021. 7. 21.

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();
  1. criteria 쿼리를 생성하려면 먼저 Criteria 빌더를 얻어야 한다. -> EntityManager나 EntityManagerFactory에서 얻을 수 있다.
  2. Criteria 쿼리 빌더에서 Criteria 쿼리를 생성한다. -> 반환 타입을 지정할 수 있다.
  3. from 절을 생성한다. 반환된 값 member는 Criteria에서 사용하는 특별한 별칭이다. -> member를 조회의 시작점이라는 의미로 쿼리 루트(Root)라 한다.
  4. 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

댓글