Programing/JPA

JPA with Springboot, 조건 조회, Specification, Predicate, CriteriaBuilder

리커니 2021. 5. 4. 14:03
반응형

JPA with Springboot, 조건 조회, Specification, Predicate, CriteriaBuilder

 

이번 포스팅의 코드는 아래의 Link 들의 코드에 이어 진행됩니다. 

아래의 Link를 참고하세요.

 

Link : aljjabaegi.tistory.com/562

 

JPA with Springboot, Entity mapping 데이터 조회 방법

JPA with Springboot, Entity mapping 데이터 조회 방법 이전 포스팅에서 JPA 설정 방법을 알아보았습니다. Link : aljjabaegi.tistory.com/561 JPA 설정 방법, Guide To JPA with Springboot JPA 설정 방법, G..

aljjabaegi.tistory.com

Link : aljjabaegi.tistory.com/563

 

JPA paging, sorting 방법 Pageable, Sort

Sprincboot JPA paging, sorting 방법 Pageable, Sort Springboot JPA 설정 방법과 Entity mapping 방법은 아래의 Link를 참고하세요. Link : aljjabaegi.tistory.com/561 JPA 설정 방법, Guide To JPA with Sprin..

aljjabaegi.tistory.com

Paging, Sorting 처리에 이어, 파라메터에 따른 조건 조회방법에 대해서 알아보도록 하겠습니다. 

 

JPA에서 조건 조회의 경우 Specification Interface를 활용합니다.

Specification의 toPredicate 메소드를 오버라이딩 해서 CriteriaBuilder 에 조건을 추가하는 방식입니다. 

CriteriaQuery를 활용한 방식도 있지만, 이번 포스팅에서는 CriteriaBuilder를 활용해 보겠습니다. 

 

우선 Repository에 JpaSpecificationExcutor를 추가 상속해줍니다. 

 

[Member Repository Interface]

import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberRepository extends PagingAndSortingRepository<Member, String>, JpaSpecificationExecutor<Member>{

}

 

기존 MemberService class에 Specification을 추가 할 수 있는 2가지 메소드를 추가 작성하겠습니다.

하나는 단일 조건 조회 Spec을 리턴하는 메소드, 하나는 복수 조건의 조회 Spec을 리턴하는 메소드 입니다.

 

단일 조건 조회의 경우 하나의 컬럼에 대한 조회만 가능한 방식입니다.

 

[단일 조건 조회 Method - getSingleSpec]

@SuppressWarnings("unused")
private Specification<Member> getSingleSpec(Map<String, Object> map){
    return new Specification<Member>() {
        private static final long serialVersionUID = 1L;
	
        @Override
        public Predicate toPredicate(Root<Member> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            Predicate p = cb.conjunction();
            if(map.get("id") != null) p = cb.and(cb.like(root.get("id"), "%"+(String) map.get("id")+"%")); /*TEXT BOX 조회*/
            if(map.get("name") != null) p = cb.and(cb.like(root.get("name"), "%"+(String) map.get("name")+"%"));
            if(map.get("useYn") != null) p = cb.and(cb.equal(root.get("useYn"), (String) map.get("useYn"))); /*COMBO BOX 조회*/
            if(map.get("regDtm") != null) p = cb.and(
                cb.between(root.get("regDtm"), (String) map.get("regDtmSt"), (String) map.get("regDtmEd"))); /*기간조회*/
            //if(map.get("regDtm") != null) p = cb.and(cb.equal(root.get("regDtm"), (String) map.get("regDtm"))); /*해당 날짜만 조회*/
            return p;
        }
	};
}

 

Client 로 부터 전달받은 Parameter Map의 값이 null이 아닐 경우

Predicate에 조건을 추가한 CriteriaBuilder를 담아 리턴해 줍니다. 

and, or 조건을 Query문에서 사용할 때와 같이 사용해주시면 됩니다.

 

like 문의 경우 like(컬럼(변수), "%"+조건값+"%")

동등비교의 경우 equal(컬럼(변수), 조건값)

between의 경우 between(컬럼(변수), start, end) 와 같이 사용하시면 됩니다.

 

복수 조건 조회의 경우 배열로 파라미터를 전달하게 되어있습니다.  

ex)

단일 조건 { "id" : "1"}

복수 조건 { "id" : ["1", "2"]}

 

[복수 조건 조회 Method - getMultiSpec]

 

@SuppressWarnings({"unused", "unchecked"})
private Specification<Member> getMultiSpec(Map<String, Object> map){
    return new Specification<Member>() {
        private static final long serialVersionUID = 1L;
		
        @Override
        public Predicate toPredicate(Root<Member> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            Predicate p = cb.conjunction();
            List<Predicate> pList = new ArrayList<Predicate>();
            /*TEXT BOX 조회*/
            if(map.get("id") != null) {
                List<String> values = (ArrayList<String>) map.get("id");
                p = cb.and(p, cb.like(root.get("id"), "%"+values.get(0)+"%"));
                for(int i=1, n=values.size(); i<n; i++) {
                    p = cb.or(p, cb.like(root.get("id"), "%"+values.get(i)+"%"));
                }
            }
            if(map.get("name") != null) {
                List<String> values = (ArrayList<String>) map.get("name");
                p = cb.and(p, cb.like(root.get("name"), "%"+values.get(0)+"%"));
                for(int i=1, n=values.size(); i<n; i++) {
                    p = cb.or(p, cb.like(root.get("name"), "%"+values.get(i)+"%"));
                }
            }
            /*COMBO BOX 조회*/
            if(map.get("useYn") != null) {
                List<String> values = (ArrayList<String>) map.get("useYn");
                List<String> list = new ArrayList<String>();
                for(String value : values) {
                    list.add(value);
                }
                p = cb.and(root.get("useYn").in(list));
            }
            /*날짜타입 조회*/
            if(map.get("regDtm") != null) {
                if(map.get("range") != null && (boolean) map.get("range")) {
                    int[][] range = null;
                    String format = "", func = "DATE_FORMAT"; /*ORACLE : TO_CHAR*/
                    /*기간 조회의 경우*/
                    List<String> values = (ArrayList<String>) map.get("regDtm");
                    String dtm = values.get(0);
                    if(dtm.length() == 16) { /*type date의 경우*/
                        range = new int[][]{{0,8},{8,16}};
                        format = "%Y%m%d";
                    }else if(dtm.length() == 24) { /*type datetime의 경우*/
                        range = new int[][]{{0,12},{12,24}};
                        format = "%Y%m%d%H%i%s";
                    }
                    p = cb.and(p, cb.between(cb.function(func, String.class, root.get("regDtm"), cb.literal(format))
                        , dtm.substring(range[0][0], range[0][1])+((dtm.length()==24)?"00":"")
                        , dtm.substring(range[1][0], range[1][1])+((dtm.length()==24)?"59":"")));
                    for(int i=1; i<values.length; i++) {
                        p = cb.or(p, cb.between(cb.function(func, String.class, root.get("regDtm"), cb.literal(format))
                            , values.get(i).substring(range[0][0], range[0][1])+((dtm.length()==24)?"00":"")
                            , values.get(i).substring(range[1][0], range[1][1])+((dtm.length()==24)?"59":"")));
                    }
                }else {
                    /*해당 날짜들로만 검색할 경우*/
                    List<String> values = (ArrayList<String>) map.get("regDtm");
                    List<String> list = new ArrayList<String>();
                    for(String value : values) {
                        list.add(value);
                    }
                    p = cb.and(cb.function("DATE_FORMAT", String.class, root.get("regDtm"), cb.literal("%Y%m%d")).in(list)); /*mariadb*/
                    //p = cb.and(cb.function("TO_CHAR", String.class, root.get("regDtm"), cb.literal("yyyyMMdd")).in(list)); /*oracle*/
                }
            }
            return p;
        }
    };
}

기능만 정상 작동하도록 구현한 코드라, 개선 할 부분은 개선해서 사용하시면 됩니다.

단일 조회와 다른점은 String을 배열 처리 해주는 부분과

처음조건은 and로 이후 조건들을 or로 처리한 부분입니다.

 

그리고 추가된 CriteriaBuilder Method는 in과 function입니다. 

in 메소드는 Query in문 처리에, function은 Database의 함수 필요 시 사용합니다. 

 

[IN문 사용]

List<String> list = new ArrayList<String>();
for(String value : values) {
    list.add(value);
}
p = cb.and(root.get("useYn").in(list));

 

[FUNCTION 사용]

p = cb.and(cb.function("DATE_FORMAT", String.class, root.get("regDtm"), cb.literal("%Y%m%d")).in(list));

 

작성된 getSingleSpec, getMultiSpec 메소드를 아래와 같이 Service class 에서 호출하여 Repository로 전달합니다.

 

@Override
public Page<Member> getGridList(Map<String, Object> map) throws Exception {
    int firstIdx = (int) map.get("pageIndex")-1;
    int lastIdx = (int) map.get("recordCountPerPage");
    Pageable paging = PageRequest.of(firstIdx, lastIdx
        , GetSort.getSort((String) map.get("sortColumn"), (String) map.get("sortOrder")));
    Specification<Member> spec = getMultiSpec(map); /*복수조건의 경우*/
    //Specification<Member> spec = getSingleSpec(map); /*단일 조건의 경우*/
    return repo.findAll(spec, paging);
}

 

서비스를 호출하면 조회 조건에 해당하는 정보가 표출되는 것을 확인하실 수 있습니다.

반응형