
저번 포스터에서 Query를 사용하는 여러가지 방법에 대해서 알아 봤습니다. 데이터를 조회하는 API를 구현할 때, 다양한 검색 조건을 처리할 수 있어야 합니다. 이렇게 유연하게 처리할 수 있게 도와주는 JPA Specificcation에 대해서 알아봅시다.
1. JPA Specification 이란?
JPA Spectification은 DB 쿼리의 조건을 Spec으로 작성하여 요청할 수 있게 해줍니다. 다양한 조건이 계속 추가 될 수록 Spec으로 관리하게 되면 코드가 깔끔해지고 유지보수가 좀 더 용이해집니다.
2. Specification 구현 준비
JPA Specification을 구현하기 위해서는 Repository에 JpaSecificationExecutor<T>
를 추가로 상속 받아야합니다.
public interface MemberRepository extends JpaRepository<Member, Integer>, JpaSpecificationExecutor<Member> {
}
상속한 JpaSpecificationExecutor
인터페이스를 살펴보자.
public interface JpaSpecificationExecutor<T> {
Optional<T> findOne(@Nullable Specification<T> spec);
List<T> findAll(@Nullable Specification<T> spec);
Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
List<T> findAll(@Nullable Specification<T> spec, Sort sort);
long count(@Nullable Specification<T> spec);
}
인터페이스에 있는 모든 메소드는 매개변수로 Specification<T>
를 받습니다. 우리는 이 Specification
을 구현해서 파라미터를 넣어주면 되는데, 아래 내용을 더 살펴 보시면 Specification
을 통해 유연한 쿼리를 작성할 수 있게 됩니다.
3. Specification 구현
이제 Specification
을 구현해봅시다. 엔티티의 여러 변수중에서 하나의 컬럼에 하나의 조건을 사용하여 검색하려고합니다. 아래는 간단하게 Member 엔티티의 memberName 컬럼에 equal로 검색하는 코드입니다.
public MemberSpecification {
public static Specification<Member> equalName(String name) {
return (Specification<Member>) ((root, query, builder) ->
builder.equal(root.get("memberName"), name)
);
}
}
root.get("memberName")
을 통해 Member 엔티티의 memberName 컬럼에 매개변수 name과 일치하는 데이터를 찾아 Member 객체를 반환합니다. 이렇게 작성한 Specification
을 이용하여 조회하는 방법은 아래와 같습니다.
memberRepository.findAll(MemberSpecification.equalName("John"));
예제는 equal()만 보았지만, 이외에도 greaterThan(), lessThan(), like(), in(), not(), isNull() 등 다양한 메소드를 지원합니다.
4. Specification 응용
위의 예제에서 간단하게 엔티티의 하나의 필드에 어떤 조건으로 검색할지 알아봤는데, 위에 코드에는 크게 2가지 문제가 있습니다.
- 여러개의 컬럼에 equal 검색하고 싶을 때
- 각 컬럼마다 eqaul 메소드를 구현 해야하는 번거러움 발생
- equal()이외에도 다른 메소드(greateThan, like, in 등)를 이용하여 검색하고 싶을 때
- 각각의 메소드마다 구현해야하며, 여러 컬럼에 대하여도 구현할 때 엄청난 코드 작성 필요
실무에서 활용하기 위해서는 여러개의 컬럼을 수용할 수 있으며, 여러 메소드를 동시에 사용할 수 있어야 한다.
이제 위의 문제를 보완한 응용코드를 보겠습니다. 먼저 어떤 컬럼에 어떤 메소드를 어떤 값으로 조회할지 검색 조건 정보를 가지는 model를 만들겠습니다.
public class Filter {
// 엔티티 속성명(컬럼)
String column;
// 값
Object value;
// 연산
Operation operation;
//@Getter, @Setter
}
public enum Operation {
GT, // GREATER_THAN
LT, // LESS_THAN
GTE, // GREATER_THAN_EQUAL
LTE, // LESS_THAN_EQUAL
NOT_EQUAL,
EQUAL,
LIKE
IN,
NOT_IN,
IS_NULL,
IS_NOT_NULL
}
column 변수는 엔티티의 속성명을 가지고, value는 검색하고자 하는 값, operation은 연산자정보를 가지는 Enum 변수입니다. Filter를 List로 받아서 처리 할 수 있는 메소드를 만들겠습니다.
public class CustomSpecification {
public static Specification<T> bySearchQuery(List<Filter> filters, Class<T> clazz) {
return (root, query, builder) -> {
List<Predicate> predicates = new ArrayList<>();
for(final Filter filter : filters) {
switch (filter.getOperation()) {
case GT:
predicates.add(builder.greaterThan(root.get(filter.getColumn()), filter.getValue().toString()));
break;
case LT:
predicates.add(builder.lessThan(root.get(filter.getColumn()), filter.getValue().toString()));
break;
case GTE:
predicates.add(builder.greaterThanOrEqualTo(root.get(filter.getColumn()), filter.getValue().toString()));
break;
case LTE:
predicates.add(builder.lessThanOrEqualTo(root.get(filter.getColumn()), filter.getValue().toString()));
break;
case NOT_EQUAL:
predicates.add(builder.notEqual(root.get(filter.getColumn()), filter.getValue().toString()));
break;
case EQUAL:
predicates.add(builder.equal(root.get(filter.getColumn()), filter.getValue().toString()));
break;
case LIKE:
predicates.add(builder.like(root.get(filter.getColumn()), filter.getValue().toString()));
break;
case IN:
predicates.add(builder.in(root.get(filter.getColumn())).value(filter.getValue()));
break;
case NOT_IN:
predicates.add(builder.not(root.get(filter.getColumn())).in(filter.getValue()));
break;
case IS_NULL:
predicates.add(builder.isNull(root.get(filter.getColumn())));
break;
case IS_NOT_NULL:
predicates.add(builder.isNotNull(root.get(filter.getColumn())));
break;
}
}
return builder.and(predicates.toArray(new Predicate[0]));
};
}
}
여러 검색 조건을 가지는 List<Filter>
를 받아서 처리하는 코드를 만들었습니다. 이제 CustomSpecification.bySearchQuery()
를 호출해서 사용해보겠습니다.
public class MemberService {
@Autowired private MemberRepository memberRepository;
public void getList(){
List<Filter> filters = new ArrayList<Filter>();
// 조건1 : memberName LIKE '%홍%'
Filter filter1 = new Filter();
filter.setKey("memberName");
filter.setValue("%홍%");
filter.setOperation(Operation.LIKE);
filters.add(filter1);
// 조건2 : age > 15
Filter filter2 = new Filter();
filter.setKey("age");
filter.setValue(15);
filter.setOperation(Operation.GT);
filter.add(filter2);
// 검색
List<Member> list = memberRepository.findAll(CustomSpecification.bySearchQuery(filters, Member.class));
}
}
'Programming > JPA' 카테고리의 다른 글
[Spring Data JPA] Query를 사용하는 여러가지 방법 (0) | 2022.08.11 |
---|---|
[Spring Data JPA] FetchType - 즉시 로딩(EAGER)과 지연 로딩(LAZY) (0) | 2022.08.04 |
[Spring Data JPA] 양방향 순환참조 문제 및 해결방법 (0) | 2022.08.03 |
[Spring Data JPA] JPA 연관관계 매핑(JOIN) (0) | 2022.08.02 |
[Spring Data JPA] JPA 주요 어노테이션(Annotation) (0) | 2022.07.31 |