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조건을 동적으로 구성하는 부분에 대해 마무리 지었고,
아직 개선해야 할 부분이 많기 때문에 더 좋은 방법에 대해 찾아 볼 예정이다.