스프링 데이터 JPA Specification 동적 쿼리 및 복합 검색

💡 핵심 요약 (Executive Summary)

본 가이드는 fw spring JPA Specification 핵심 가이드 및 실전 아키텍처의 핵심 개념, 실전 구성 명령어 및 아키텍처 베스트 프랙티스를 다룹니다. 클라우드 네이티브 및 인프라 엔지니어링 실무에 즉시 활용 가능하도록 체계적으로 정리했습니다.

Reference & Guidelines (NOTE)

JPA Creteria 사용시 유지보수가 힘들어질 것을 대비하여 자주사용될 구문을 static method로 준비함. (static method 제네릭 문법으로 정의)

java
/** Replace to Global
1. $package
2. $RepositoryName
3. $EntityName
4. $columnVarName
5. $EntityName2 (for InnerJoin)
6. $entity2VarName (for InnerJoin)
*/
how to use
java
import $package.SpecificationUtil;
import static $package.SampleEnum.SAMPLE;

private final $RepositoryName repo;
/* in Method */

// 초기화: 이렇게 초기화 하면 바로 .and()로 조건 추가할 수 있음.
Specification<$EntityName> spec = (root, query, criteriaBuilder) -> null;

// or조건 사용, NOT IN 사용
List<String> list;
spec = spec.or(SpecificationUtil.notIn("$columnVarName", list));

// and조건 사용, = 사용
String str;
spec = spec.and(SpecificationUtil.equal("$columnVarName", str));

// enum 사용 (repository columln 타입이 enum인 경우)
spec = spec.and(SpecificationUtil.equal("$columnVarName", SAMPLE));

// innerjoin하여 확장된 컬럼에 대한 IN 사용 (repository에 조인되어 있어야 함)
List<String> list;
spec = spec.and(SpecificationUtil.inInnerJoin(
                    $EntityName2.class,
                    "$entity2VarName",
                    "$repoColumnVarName",
                    list
            ));

// Specification을 조건으로 사용하여 검색
List<$EntityName> entities = repo.findAll(spec);
repository Class
java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface $RepositoryName extends JpaRepository<$EntityName, Long>, JpaSpecificationExecutor<$EntityName> {
		// jpa repository
		@Column(name = "VALUE")
    private String $columnVarName;

		@ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "JOIN_KEY")
    private $EntityName2 $entity2VarName;
}
static Method (util) Class
java
package cloudPlatform.cmm.jpa;

import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import java.util.Arrays;
import java.util.List;

public static <T> Specification<T> isNull(String colName) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.isNull(root.get(colName));
    }

    public static <T> Specification<T> isNotNull(String colName) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.isNotNull(root.get(colName));
    }

    public static <T> Specification<T> lessThanOrEqualTo(String colName, String value) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.lessThanOrEqualTo(root.get(colName), value);
    }

    public static <T> Specification<T> greaterThanOrEqualTo(String colName, String value) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.greaterThanOrEqualTo(root.get(colName), value);
    }

    public static <T> Specification<T> lessThan(String colName, String value) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.lessThan(root.get(colName), value);
    }

    public static <T> Specification<T> greaterThan(String colName, String value) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.greaterThan(root.get(colName), value);
    }

    public static <T, V> Specification<T> notIn(String colName, List<V> valueList) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.not(root.get(colName).in(valueList));
    }

    public static <T, V> Specification<T> in(String colName, List<V> valueList) {
        return (root, query, criteriaBuilder) -> root.get(colName).in(valueList);
    }

    public static <T, V> Specification<T> like(String colName, V value) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get(colName), "%" + value + "%");
    }

    public static <T, V> Specification<T> equal(String colName, V value) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(colName), value);
    }

    public static <T, V> Specification<T> notEqual(String colName, V value) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.notEqual(root.get(colName), value);
    }

    public static <T1, T2, V> Specification<T1> inInnerJoin(Class<T2> joinEntityClass, String colName, List<V> valueList) {
        return (root, query, criteriaBuilder) -> {
            String joinEntityName = Arrays.stream(root.getJavaType().getDeclaredFields())
                    .filter(field -> field.getType() == joinEntityClass)
                    .map(Field::getName)
                    .findFirst()
                    .orElse(null);
            Join<T1, T2> joinEntity = root.join(joinEntityName, JoinType.INNER);
            return joinEntity.get(colName).in(valueList);
        };
    }

    public static <T1, T2, V> Specification<T1> equalsInnerJoin(Class<T2> joinEntityClass, String colName, V value) {
        return (root, query, criteriaBuilder) -> {
            String joinEntityName = Arrays.stream(root.getJavaType().getDeclaredFields())
                    .filter(field -> field.getType() == joinEntityClass)
                    .map(Field::getName)
                    .findFirst()
                    .orElse(null);
            Join<T1, T2> joinEntity = root.join(joinEntityName, JoinType.INNER);
            return criteriaBuilder.equal(joinEntity.get(colName), value);
        };
    }
}
old

#### Specification 사용법

##### 개요

> 💡

드림마크원 감사로그 다중검색기능 추가시 WHERE절을 동적으로 컨트롤 하기 위하여 분석된 기능

JPA 추가 인자값.

1. Pageable : Order By, Limit Offset 등의 페이징처리 정보 객체
2. Specification : and, or로 추가된 WHERE 조건구성 정보 객체

##### Repository extends

java
public interface AuditLogRepository extends JpaRepository<AuditLog, Long>, JpaSpecificationExecutor<AuditLog> {}
// public interface jpaRepository enxtends JpaRepository<{ Entity }, Long>, JpaSpecificationExecutor<{{ Entity }}>

위와같이 repository를 선언할 때 JpaRepository 뿐만이 아닌 JpaSpecificationExecutor도 상속한다.

→ 결과적으로 findAll에 첫번째 인자값으로 Specification\<\{ Entity \}>를 사용할 수 있게 된다.

→ ex) findAll(Specification); findAll(Specification, Pageable);

##### 조건 정의

– Specification에 담기위한 조건을 사전 정의한다.

java
package cloudPlatform.cmm.audit.repo.spec;

import cloudPlatform.cmm.audit.model.AuditLog;
import org.springframework.data.jpa.domain.Specification;

import java.util.List;

public class AuditSpecification {
    public static Specification<AuditLog> notIn(String colName, List<String> valueList) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.not(root.get(colName).in(valueList));
    }
    public static Specification<AuditLog> in(String colName, List<String> valueList) {
        return (root, query, criteriaBuilder) -> root.get(colName).in(valueList);
    }
    public static Specification<AuditLog> like(String colName, String value) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get(colName), "%" + value + "%");
    }
    public static Specification<AuditLog> lessThanOrEqualToCreatDt(String value) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.lessThanOrEqualTo(root.get("creatDt"), value);
    }
    public static Specification<AuditLog> greaterThanOrEqualToCreatDt(String value) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.greaterThanOrEqualTo(root.get("creatDt"), value);
    }
    public static Specification<AuditLog> isNotConnSection() {
        return (root, query, criteriaBuilder) -> criteriaBuilder.notLike(root.get("se"), "로그%");
    }
    public static Specification<AuditLog> isConnSection() {
        return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("se"), "로그%");
    }
    public static Specification<AuditLog> betweenAndEqualTo(String colName, String from, String to) {
        Specification<AuditLog> rtnSpec = (root, query, criteriaBuilder) -> criteriaBuilder.greaterThanOrEqualTo(root.get(colName), from);
        rtnSpec.and(((root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.lessThanOrEqualTo(root.get(colName), to)));
        return rtnSpec;
    }
}

1. Specification<{ Entity }>로 리턴하는 값은 항상 return (root, query, criteriaBuilder) -> { in / like / equal / less / greater / ... }를 이용할 수 있음
1. 비교할 value가 List인 경우 root.get(”colName”).method(valueList)
2. 비교할 value가 String인 경우 criteriaBuilder.like(root.get(”colName”), value)
2. 기본 로직은 다음과 같음

java
// Ex: "TableVo" Entity, "column" variable에 대하여
   // List를 포함하는레코드만 조회한 후 val을 포함하는 문자열만 조회

   Specification<TableVo> mainSpec = (root, query, criteriaBuilder) -> null;
   Specification<TableVo> inSpec = (root, query, criteriaBuilder) -> root.get("column").in(List);
   Specification<TableVo> likeSpec = (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("column"), "%" + val + "%");

   mainSpec.and(inSpec);
   mainSpec.and(likeSpec);

   // Specification만 사용시
   List<TableVo> resultList = repository.findAll(mainSpec);

   // Sort와 함께 사용시 : "column" 기준 오름차순 정렬
   Sort sort = Sort.by("column").ascending(); //.descending();
   List<TableVo> sortingList = repository.findAll(mainSpec, sort);

   // Pageable과 함께 사용시
   Sort sort = Sort.by("column").ascending(); //.descending();
   Pageable pageable = PageRequest.of( { pageNum }, { pageSize }, sort );
   Page<TableVo> pagingList = repository.findAll(mainSpec, pageable);

3. 위에서 사용한 컬럼명 “colName”자리의 String value는 Entity 내 비교대상의 변수명과 일치해야 한다.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *