일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- RTK
- backend
- storybook
- Gin
- 티스토리챌린지
- satisfiles
- 오블완
- javascript
- JavaSpring
- component
- frontend
- tanstackquery
- test
- go
- Spring
- hook
- springboot
- java
- react-hook-form
- designpatterns
- Chakra
- React
- Redux
- css
- golang
- 웹애플리케이션서버
- ReactHooks
- JPA
- typescript
- Today
- Total
bkdragon's log
Specification 과 Criteria API 본문
Specification
명세, 조건이라는 뜻으로 복잡한 데이터 검색 조건을 간단하고 유연하게 구현할 수 있는 Spring Data JPA의 기능이다.
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
Specification은 Predicate를 반환하는 toPredicate라는 함수를 가지는 인터페이스이다. Predicate는 아래에서 설명할 JPA의 Criteria API에서 사용하는 쿼리의 조건을 정의하는 객체이다.
즉 Specification은 Predicate을 추상화하여 사용하는 것이다.
Criteria API 란?
JPA(Java Persistence API)의 일부로, 프로그래매틱하게 타입 세이프한 쿼리를 생성할 수 있는 API이다.
주요 개념
CriteriaBuilder
: 쿼리의 구성요소를 생성한다. 조건 표현식, Predicate(술어), 정렬 순서 등을 생성한다. Equal, Like, GreaterThan 등의 조건을 만들고 (where), and 나 or을 통해 조건을 합치고 (and, or), 집계 함수나 정렬 조건을 만들 수 있다.EntityManager
의getCriteriaBuilder()
메서드를 통해 인스턴스를 얻을 수 있다.CriteriaQuery
: 쿼리 자체를 구성하고 수정하는데 사용된다.Root
: 쿼리의 FROM 절에 해당하는 엔티티를 지정하는데 사용된다. 해당 엔티티의 속성에 접근할 수 있다.Predicate
: 쿼리의 WHERE 절 조건을 나타냅니다. 조건을 구성하고, 조합하여 복잡한 검색 조건을 정의할 수 있다.Expression
: 쿼리 내에서 사용되는 연산을 나타내는 객체입니다. 예를 들어, 엔티티의 속성 값, 집계 함수 등을 표현할 때 사용된다.
연관성
Criteria API 의 목적은 자바 코드로 쿼리를 작성함에 있는데 그것을 더 쉽게 사용할 수 있게 만들어진 레이어를 Specification으로 보면 될 것 같다.
사용법
우선 Criteria API 의 사용법을 알아보자.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Entity> cq = cb.createQuery(Entity.class);
Root<Entity> root = cq.from(Entity.class);
Predicate condition = cb.equal(root.get("attribute"), "value");
cq.where(condition);
List<Entity> result = entityManager.createQuery(cq).getResultList();
엔티티 메니저로 부터 쿼리빌더를 얻고 쿼리빌더로부터 쿼리를 얻고 쿼리로 부터 루트를 얻는다.
쿼리 빌더의 equal을 통해 루트의 특정 어트리뷰트와 특정 변수가 같아야한다는 조건을 만들고 쿼리에 포함시킨다.
포인트는 쿼리빌더가 Predicate를 반환하고 그 Predicate를 쿼리에 포함시켜 사용한다는 것이다.
이제 Specification의 사용법을 알아보자. Specification은 좀 더 조건을 더 쉽게 생성하는데 포커스가 맞춰진 느낌이다. (where 절을 쉽게)
처음에 Specification을 설명하는 부분에 보면 toPredicate라는 메서드가 있는데 Specification은 그 형태를 만들어서 사용하면 된다.
public class EntitySpecification {
public static Specification<Entity> hasAttribute(String value) {
return (root, query, cb) -> cb.equal(root.get("attribute"), value);
}
}
hasAttribute 의 형태를 보면 toPredicate 메서드와 같은 형태를 반환한다. toPredicate가 반환하는 건 Predicate이다. Predicate를 한번 감싼 형태이다.
public List<RequestResponse> findAllByCondition(UUID companyId, String designerId, UUID companyUserId, List<RequestStatus> status, String searchKeyword) {
Specification<Request> spec = Specification.where(null);
if (companyId != null) {
spec = spec.and(RequestSpecification.withCompanyId(companyId));
}
if (designerId != null) {
spec = spec.and(RequestSpecification.withDesignerId(designerId));
}
if (companyUserId != null) {
spec = spec.and(RequestSpecification.withCompanyUserId(companyUserId));
}
if (status != null) {
spec = spec.and(RequestSpecification.withStatusList(status));
}
if(searchKeyword != null) {
spec = spec.and(RequestSpecification.withDynamicQuery(searchKeyword));
}
List<Request> all = requestRepository.findAll(spec);
return RequestResponse.from(all);
}
서비스 계층에서 이런 식으로 사용이 되는데 and 메서드로 Specification 들을 합치고 있다. 최종적으로 findAll에 인자로 넘겨져 쿼리를 구성하는데 사용될 것이다.
검색 조건을 넣으면 조금 복잡해진다.
public static Specification<Request> withDynamicQuery(String search) {
return (root, query, builder) -> {
List<Predicate> predicates = new ArrayList<>();
if (search != null && !search.isEmpty()) {
// 제목 검색 조건
predicates.add(builder.like(builder.lower(root.get("name")), "%" + search.toLowerCase() + "%"));
// 디자이너 이름 검색 조건, 연관 필드 검색
predicates.add(builder.like(builder.lower(root.join("designerUser").get("username")), "%" + search.toLowerCase() + "%"));
// 회사 이름 검색 조건, 연관 필드 검색
predicates.add(builder.like(builder.lower(root.join("company").get("name")), "%" + search.toLowerCase() + "%"));
// 회사 사용자 이름 검색 조건, 연관 필드 검색 및 null 체크
Predicate companyUserPredicate = builder.isNotNull(root.get("companyUser"));
predicates.add(builder.and(companyUserPredicate, builder.like(builder.lower(root.join("companyUser").get("username")), "%" + search.toLowerCase() + "%")));
}
return builder.or(predicates.toArray(new Predicate[0]));
};
}
bulider의 like를 통해 Predicate를 만들어서 predicates list 에 추가하고 있다.
그냥 name, designerUser의 username, company의 name, companyUser 의 username까지 비교를한다.
companyUser의 경우는 null 일 수도 있어서 null이 아니여야 한다는 Predicate를 만들고 and를 사용해 두 Predicate 결합한다. 그래서 null 아니면 companyUser의 username까지 비교하게 된다.
마지막에 builder.or로 모든 Predicate를 합친다.
query는 그룹화, 정렬, 조인등에 사용된다.
root.join("associatedEntity", JoinType.LEFT); // query 매개변수는 사용하지 않지만, query의 컨텍스트 내에서 조인을 정의
query.groupBy(root.get("attribute")); // 결과를 특정 속성으로 그룹화
query.having(builder.gt(builder.count(root), 1)); // 그룹화된 결과에 대한 조건 적용
query.orderBy(builder.asc(root.get("attribute"))); // 특정 속성에 대해 오름차순 정렬
public static Specification<Todo> orderByEndDateDesc() {
return (root, query, builder) -> {
if (query.getResultType() != Long.class && query.getResultType() != long.class) {
query.orderBy(builder.desc(root.get("endDate")));
}
return builder.conjunction();
};
}
query 를 사용하는 부분은 Criteria API 와 똑같다. 여기부턴 Specification의 장점이 없어진다고 생각한다.
conjunction는 Predicate를 추가하지 않았을 때 사용할 수 있다.
서브쿼리를 사용하는 예시도 살펴"는" 보자 (기억하진 말자 .. 굳이...)
SELECT r.*
FROM requests r
WHERE r.designer_user_id IN (
SELECT dgm.designer_user_id
FROM designer_group_members dgm
JOIN designer_groups dg ON dgm.designer_group_id = dg.id
WHERE dg.id = :designerGroupId
)
public static Specification<Request> withDesignerGroupId(UUID designerGroupId) {
return (root, query, builder) -> {
if (designerGroupId == null) return null;
Subquery<DesignerUser> designerUserSubquery = query.subquery(DesignerUser.class); // DesignerUser 를 반환하는 서브쿼리를 생성
Root<DesignerGroupMember> designerGroupMemberRoot = designerUserSubquery.from(DesignerGroupMember.class); // Root 설정, from 뒤에 오는 엔티티
designerUserSubquery.select(designerGroupMemberRoot.get("designerUser")); // SELECT dgm.designerUser FROM designerGroupMembers dgm
Predicate designerGroupPredicate = builder.equal(designerGroupMemberRoot.get("designerGroup").get("id"), designerGroupId); // 조건 추가, dgm.designerGroup.id = :designerGroupId
designerUserSubquery.where(designerGroupPredicate); // 조건을 WHERE 로 합침.
return builder.in(root.get("designerUser")).value(designerUserSubquery); // 서브쿼리와 in 으로 조합, 결국 이 조건에 맞는 Request 를 얻게 된다.
};
}
Specification은 조건부 조회에만 사용되는게 좋아보인다. 검색 조건이라던가 기간에 대한 조건이 있을 때 활용하면 가장 장점을 보이는 것 같다.
grouBy 나 subquery 같은 복잡한 쿼리는 당연하고, orderBy와 같은 비교적 간단한 쿼리도 Specification으로 사용하는 건 좋은 선택이 아닌 것 같다.
'Java Spring' 카테고리의 다른 글
웹 기술의 발전과정 (0) | 2024.11.12 |
---|---|
JPA와 하이버네이트 (2) | 2024.10.24 |
Transactional 원리 (0) | 2024.09.24 |
JDBC 부터 Spring Data JPA 까지 (0) | 2024.09.20 |