Back/JPA

JPA Specification으로 findAll 조회쿼리 where조건 동적 세팅

밍꿔 2020. 4. 28. 10:34


반응형

 

Mybatis를 사용하다 JPA를 사용해보니, 데이터 select시 조건세팅에 대한 부분이 고민되었다.

 

Mybatis에서는 xml Mapper로 넘어온 파라미터 여부에 따른 조건절을 동적으로 세팅하는 부분이 눈에

 

보이기 때문에 어떻게 보면 JPA보다 직관적으로 파악 할 수 있다고 생각한다.

 

JPA에서 각 파라미터에 따른 find메서드를 생성할수도 없고, 물론 설계나 어떤 요건에 따른 특화된

 

기능이라면 그렇게 사용하는것이 좋지만,

 

공통 조회서비스에서 각 파라미터 별로 메서드를 생성하긴 비효율적이다.

 

때문에 Mybatis에서 사용했던 내용처럼 Parameter 객체에 모든 파라미터를 담아 그 객체만 넘겨서

 

동적으로 select하는 방식을 구성해봤다. 

 

 

1. User Repository

import com.example.study.model.entity.User;
import com.example.study.model.enumclass.UserStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User,Long> {
	
    // 1. 계정 조건 find
    Page<User> findAllByAccount(Pageable pageable, String account);
    // 2. 상태 조건 find
    Page<User> findAllByStatus(Pageable pageable, UserStatus status);
    // 3. 계정, 상태 조건 find
    Page<User> findAllByAccountAndStatus(Pageable pageable, String account, UserStatus status);
    // 4. Specification를 이용하여 동적으로 조건을 세팅하여 find
    Page<User> findAll(Specification<User> spec, Pageable pageable);
    
}

 

위에서 말했던 내용처럼,

 

account를 조회 하는 findAllByAccout

 

Status를 조회 하는 findAllByStatus,

 

account, Status를 동시에 조회 하는 findAllByAccountAndStatus

 

1, 2, 3번 처럼 각 조건이 추가 될때마다 find메서드를 생성하는 방식이 아닌,

 

4번과 같은 방식을 진행 하려고 한다.

 

 

2. UserService의 search메서드 호출 부분.

public Header<List<UserApiResponse>> search(Pageable pageable, UserApiRequest userApiRequest) {
	// Json형태의 userApiRequest객체를 Map형태로 convert
	Map<String, Object> searchRequest = CommonObjectUtils.convertObjectToMap(userApiRequest);
    	// where조건 Parameter Map
	Map<String, Object> searchKeys = new HashMap<>();

	// Parameter 순차적으로 세팅
	for (String key : searchRequest.keySet()) {
		String value = String.valueOf(searchRequest.get(key));
		if(value != null && !value.isEmpty() && !"null".equals(value)){
			searchKeys.put(key, searchRequest.get(key));
		}
	}

	// Parameter 존재 여부에 따른 메서드 분기처리
	Page<User> users = searchKeys.isEmpty() ?
	userRepository.findAll(pageable) :
	userRepository.findAll(userSpecification.searchWith(searchKeys), pageable);

	// DataList Set
	List<UserApiResponse> userApiResponseList = users.stream()
	.map(user -> response(user))
	.collect(Collectors.toList());

	// Paging Navigation
	Pagination pagination = Pagination.builder()
	.totalPages(users.getTotalPages())
	.totalElements(users.getTotalElements())
	.currentPage(users.getNumber())
	.currentElements(users.getNumberOfElements())
	.build();

	return Header.OK(userApiResponseList,pagination);
}

 

Specification를 사용하여 파라미터 세팅하는 부분을 어떻게 넘겨주는지에 대한 내용이고

 

실제 Specification사용하는 내용은 아래에.

 

 

3. User Specification

import com.example.study.common.CommonObjectUtils;
import com.example.study.model.entity.Item;
import com.example.study.model.entity.Partner;
import com.example.study.model.entity.User;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Component;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class UserSpecification {

    public static Specification<Item> searchWith(Map<String, Object> searchKeyword) {
        return (Specification<Item>) ((root, query, builder) -> {
            List<Predicate> predicate = getPredicateWithKeyword(searchKeyword, root, builder);
            return builder.and(predicate.toArray(new Predicate[0]));
        });
    }

    private static List<Predicate> getPredicateWithKeyword(Map<String, Object> searchKeyword, Root<Item> root, CriteriaBuilder builder) {
        List<Predicate> predicate = new ArrayList<>();
        for (String key : searchKeyword.keySet()) {
            if("name".equals(key)){ //'name' 조건은 like 검색
                predicate.add(builder.like(root.get(key), "%"+searchKeyword.get(key)+"%"));
            }else if("partner".equals(key)){ // 'partner' 조건은 partner객체 안에 있는 keword데이터를 2차 가공하여 검색
                Join<Item,Partner> join = root.join("partner");
                Map<String, Object> partnerKeyword = CommonObjectUtils.convertObjectToMap(searchKeyword.get(key));
                for (String partnerKey : partnerKeyword.keySet()) {
                    if("name".equals(partnerKey)){
                        predicate.add(builder.like(join.get("name"), "%"+ partnerKeyword.get("name")+"%"));
                    }
                }
            }else{ // 'name', 'partner' 이외의 모든 조건 파라미터에 대해 equal 검색
                predicate.add(builder.equal(root.get(key), searchKeyword.get(key)));
            }
        }
        return predicate;
    }
    
    /*
    private static List<Predicate> getPredicateWithKeyword(Map<String, Object> searchKeyword, Root<User> root, CriteriaBuilder builder) {
        List<Predicate> predicate = new ArrayList<>();
        for (String key : searchKeyword.keySet()) {
            predicate.add(builder.equal(root.get(key), searchKeyword.get(key)));
        }
        return predicate;
    }
    */

}

 

특정조건(name, partner)에 따라 조건세팅 부분을 분기 처리 가능하고,

 

그 이외의 다른 조건들은 equal 조회가 가능하다.

 

조회 하는데 예외케이스가 없다면 주석처리된 내용처럼 비교적 간단하게 구현 가능하다.

 

1차적으로 where조건을 동적으로 구성하는 부분에 대해 마무리 지었고,

 

아직 개선해야 할 부분이 많기 때문에 더 좋은 방법에 대해 찾아 볼 예정이다.

 

 

 

 

반응형