Spring발행일 2024. 8. 19.원본 https://blog.naver.com/jword_/223551020815 ↗

[자바 스프링] criteria api 기본기 다지기

[자바 스프링] criteria api 기본기 다지기 — #자바스프링 #자바criteriaapi #criteriaapi #자바쿼리 #개발자의도구들 🚨📝 📖 📒✏️💡🔍 ...

#Spring#Naver Blog

#자바스프링 #자바criteriaapi #criteriaapi #자바쿼리 #개발자의도구들

🚨📝 📖 📒✏️💡🔍

​

AI스쿨 msa기반 java 백엔드 코스 중에 공부한 내용을 작성하였습니다

\* 현재 Spring 프로젝트를 진행 중입니다. 전체적인 목차를 보시려면 여기를 눌러주세요.

이미지

복습하고 가요

이전글에서 JPQL로 복잡한 Query를 날리는걸 구현해 보았는데요. 이번에는 JPQL이 아닌 Criteria Api를 사용하여 복잡한 쿼리를 날려보도록 하겠습니다.

​

그전에 JPQL에 대해서 간단하게 복습해볼까요?

  • JQPL은 EntityManger를 사용하여 쿼리를 날릴때 사용된다.
  • SQL과 구문이 비슷한데, 큰 차이점은 Entity class 기준으로 Query를 작성한다는 것이다.
  • 복잡한 쿼리를 만들기에 유리하다.

​

나타난 문제점

JPQL은 동적쿼리를 생성하기에 적합하지 않습니다. 이유는 JPQL이 다음과 같은 특징이 있기 때문인데요.

​

  • 고정된구조: 쿼리의 구조와 조건이 코드에서 명확히 정의되며, 실행 시점에 변하지 않습니다

​

이전글에 CustomCriteria객체를 만들어서 조건을 전달하였었는데요.

package com.example.webserver.dto; ​ import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; ​ @Builder @RequiredArgsConstructor @Getter public class CustomCriteria { private final String writer; private final String notContainWriter; private final Integer minReads; private final Integer maxReads; }
@Override public List<CommentEntity> findByCustomCriteria(CustomCriteria customCriteria) { return entityManager.createQuery( "SELECT c FROM CommentEntity c " + "JOIN c.board b " + "WHERE b.writer LIKE :writer " + "AND b.writer NOT LIKE :notContain " + "AND (b.readingCount > :minReads " + "OR b.readingCount <= :maxReads)" , CommentEntity.class) .setParameter("writer", customCriteria.getWriter()) .setParameter("notContain", customCriteria.getNotContainWriter()) .setParameter("minReads", customCriteria.getMinReads()) .setParameter("maxReads", customCriteria.getMaxReads()) .getResultList(); }

여기서 만약 writer이 없는 경우의 코드는 어떻게 변해야 할까요?

​ @Override public List<CommentEntity> findByCustomCriteria(CustomCriteria customCriteria) { if (customCriteria.getWriter() == null) { return entityManager.createQuery( "SELECT c FROM CommentEntity c " + "JOIN c.board b " + "WHERE b.writer NOT LIKE :notContain " + "AND (b.readingCount > :minReads " + "OR b.readingCount <= :maxReads)" , CommentEntity.class) .setParameter("notContain", customCriteria.getNotContainWriter()) .setParameter("minReads", customCriteria.getMinReads()) .setParameter("maxReads", customCriteria.getMaxReads()) .getResultList(); } return entityManager.createQuery( "SELECT c FROM CommentEntity c " + "JOIN c.board b " + "WHERE b.writer LIKE :writer " + "AND b.writer NOT LIKE :notContain " + "AND (b.readingCount > :minReads " + "OR b.readingCount <= :maxReads)" , CommentEntity.class) .setParameter("writer", customCriteria.getWriter()) .setParameter("notContain", customCriteria.getNotContainWriter()) .setParameter("minReads", customCriteria.getMinReads()) .setParameter("maxReads", customCriteria.getMaxReads()) .getResultList(); }

이런식으로 null조건을 체크한 후 JPQL을 새로 다시 작성해야합니다. 지금 feild가 4개라서 망정이지, 만약 feild가 훨씬 많아진다면, 그만큼 JPQL 쿼리를 새로 다 작성을 해야하는데요. 매우 번거로운 일이며 가독성도 너무 떨어집니다.

​

이때는 ​Criteria api를 사용하여 복잡한 쿼리 작성없이 동적쿼리를 생성하여 데이터를 조회할 수 있습니다.

Criteria Api

Criteria api는 JPA의 EntityManger가 제공하는 api입니다. 이전글을 잘 따라오셨다면 따로 환경설정을 핦 필요는 없으시고, 만약 JPA셋팅이 안돼어 있으시다면 이글을 참고해주세요.

​

기본 코드 형태

@Override public List<BoardEntity> findByWriterLikeByCriteria(String writer) { CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); CriteriaQuery<BoardEntity> criteriaQuery = criteriaBuilder.createQuery(BoardEntity.class); Root<BoardEntity> board = criteriaQuery.from(BoardEntity.class); ​ Predicate predicate = criteriaBuilder.like(board.get("writer"), "%" + writer + "%"); criteriaQuery.where(predicate); TypedQuery<BoardEntity> query = entityManager.createQuery(criteriaQuery); return query.getResultList(); }

이전글에서 구현한 findAll() 메서드를 CriteriaApi로 구현한 코드입니다. 조금 복잡해 보이는데요. 이유는 아마도 생소한 Class들 때문이라고 생각됩니다.

​

이 Class들을 전격 분석하여 어떻게 사용하는지 알아보도록 해요.

CriteriaBuilder

CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

CriteriaBuilder는 가장 뿌리가 되는 클래스입니다. Query객체를 생성하며, 다양한 조건(Predicate)을 생성하는데 사용될 수 있습니다.

Predicate predicate = criteriaBuilder.like(board.get("writer"), "%" + writer + "%");

위의 코드에서도 criteriaBuilder를 사용하여 like라는 Where절의 조건을 생성하는 것을 볼 수 있습니다.

​

주요 Method

CriteriaBuilder cb = entityManager.getCriteriaBuilder(); ​ cb.equal cb.asc cb.desc cb.and cb.or cb.like cb.notEquale cb.exist cb.gt cb.lt cb.between ​ // 대부분 조건절에서 사용되는 표현식을 지원합니다.

CriteriaQuery

CriteriaQuery는 Criteria API에서 생성된 쿼리를 표현하는 객체로, 쿼리의 전체 구조를 나타내기 위해 사용됩니다.

SELECT ... DISTINCT .... FROM ... WHERE.. ORDERBY...HAVING...(GROUP BY)

SQL 쿼리에서의 전체 구조인 SELECT, FROM, WHERE과 같이 SQL에서 큰 절에 해당하는 구문을 담당합니다..

CriteriaQuery<BoardEntity> criteriaQuery = criteriaBuilder.createQuery(BoardEntity.class);

선언시 Entity타입을 지정하고 있는데, 이는 최종 return하게될 Entity를 지정하는 것 입니다.

\* 참고로 FROM과는 전혀 다른 개념이니 기억하시기 바랍니다.

​

주요 Method

// \조회\ select(Selection ) // 반환 filed 지정 distinct(boolean ) // 중복 쿼리 제거 여부 결정 multiselect(List selectionList) // 반환 filed 선택 지정 multiselect(Selection selections) // 위와 동일 ​ // \조건\ where(Expression restriction) where(Predicate restriction) // 조건 조회 (Preicate와 Expression은 조건을 나타냄) groupBy(Expression grouping) groupBy(List grouping) // 쿼리 결과에 대한 그룹을 형성 having(Expression restrcition) having(Predicate restriction) // 쿼리 그룹에 대한 제한 지정 (having 절) ​ // \정렬\ orderBy(List) orderBy(Order) // orederBY getParameters() //쿼리의 매개변수 반환 getOrderList() // 우선순위에 따라 표현식 반환

Root

Root객체는 from절을 지정하는데 사용됩니다. 즉 Query를 조회할 table을 지정하는 것과 같습니다.

Root<BoardEntity> board = criteriaQuery.from(BoardEntity.class);

코드를 보시면 아시겠지만, criteriaQuery.from으로 부터 지정됩니다. 이는 위에서 언급했듯이 criteriaQuery가 SELECT... FROM ...WHERE과 같은 절을 담당하기 떄문입니다.

Predicate

조건을 저장(?) 하는데 사용되는 객체입니다.

Predicate predicate = criteriaBuilder.like(board.get("writer"), "%" + writer + "%");

앞서 언급했듯이 조건은 criteriaBuilder로 부터 파생되어 나올 수 있습니다.

criteriaQuery.where(predicate);

이 조건절은 criteraQuery.where을 통해 쿼리의 WHERE문으로 들어갑니다.

쿼리 날리기

TypedQuery<BoardEntity> query = entityManager.createQuery(criteriaQuery); return query.getResultList();
💡TypeQuery<Entity> 사용이유 ​ 타입 안정성을 보장합니다. entityManger.createQuery의 결과를 TypeQuery<Entiy>로 감쌉니다. 이는 entityManger.createQuery의 결과를 다른 Entity로도 변환하여 return 할 수 있도록 만듭니다. (타입 안정성)

​

동적 쿼리 생성에 유리한 Criteria api

자, 이제 동적쿼리 생성에 어떻게 유리한지 알아봅시다. 우선 상황을 가정해야하는데요.

​

🔍 특정 조회수 이상(초과), 혹은 이하(미만), 혹은 사이의 BoardEntity를 구해야한다.

​

이 경우 사용자의 request 메세지로 부터 날아올 수 있는 경우의 수를 모두 고려해 보겠습니다. 우선 경우의 수를 고려하여 조건에 해당하는 객체를 만들어볼게요.

​

package com.example.webserver.dto; ​ import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; ​ @Builder @Getter @RequiredArgsConstructor public class ReadingCountCriteria { private final Integer minReads; private final Integer maxReads; private final boolean isMinEqual; private final boolean isMaxEqual; private final boolean isBetween; } ​

💼 minReads or maxReads 각 하나만 있는 경우

  • minReads만 있는 경우 (>)
  • minReads와 isMinEqual이 있는 경우 (>=)
  • maxReads만 있는 경우 (<)
  • maxReads와 isMaxEqual만 있는 경우( <=)

​

💼minReads와 maxReads가 동시에 있는 경우

👛isBetween인 경우 (< <)

  • isBetween과 isMinEqual만 있는 경우 (<= <)
  • isBetween과 isMaxEqual만 있는 경우 (< <=)
  • isBetween과 isMinEqaul과 isMaxEqual이 있는 경우 (<= <=)

​

👛 isBetween이 아닌 경우 ( < or >)

  • isMinEqaul만 있는 경우 (< or >=)
  • isMaxEqaul만 있는 경우 (<= or >

​

하나 하나 다 따져보니 생각보다 많죠?? 이걸 JPQL로 하나 하나 나눠서 query를 작성하는건 정말 힘들 것 같아요.

동적 쿼리 대응이 쉬운 Criteria Api

자 그럼 이제부터 Criteria Api의 찐힘을 함꼐 봅시다!

@Override public List<BoardEntity> findByReadingCountWithCriteria(ReadingCountCriteria criteria) { CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); CriteriaQuery<BoardEntity> criteriaQuery = criteriaBuilder.createQuery(BoardEntity.class); Root<BoardEntity> board = criteriaQuery.from(BoardEntity.class); ​ if (criteria.getMaxReads() != null && criteria.getMinReads() != null) { if (criteria.isBetween()) { if (criteria.isMaxEqual() && criteria.isMinEqual()) { Predicate predicate = criteriaBuilder.between(board.get("readingCount"), criteria.getMinReads(), criteria.getMaxReads()); } else if (criteria.isMaxEqual()) { Predicate maxPredicate = criteriaBuilder.lessThanOrEqualTo(board.get("readingCount"), criteria.getMaxReads()); Predicate minPredicate = criteriaBuilder.greaterThan(board.get("readingCount") , criteria.getMinReads()); } } } }

이런식으로 조건을 나눠서 해당 method만 호출하여 쿼리를 날려주면 됩니다! (시간관계쌍 모든 케이스에 대해서 쿼리 작성은 못햇어요)

​

나머지는 여러분들이 직접 작성해보면서 api에 익숙해지면 더 좋을 것 같아요. 사실 저거 적는 것만 해도 어지러운데, 실제로 jPQL을 모두 작성하는건 너무 힘들겠다고 생각이 드네요 :>