주요글: 도커 시작하기

스프링 데이터 JPA 기능 중에서 Pageable과 Page를 사용하면 쉽게 페이징 처리를 할 수 있어 편리하다. 하지만 특정 행부터 일정 개수의 데이터를 조회하고 싶은 경우에는 Pageable과 Page가 적합하지 않다(예를 들어 21번째 행부터 21개의 데이터를 읽어오고 싶은 경우). 특정 행부터 일정 개수의 데이터를 조회할 수 있는 기능을 모든 리포지토리에 적용할 필요가 생겼는데 이를 위해 다음 작업을 진행했다.

  • Rangeable 클래스 추가 : 조회할 범위 값 저장(Pageable 대체).
  • RangeableExecutor 인터페이스 : Rangeable 타입을 사용하는 조회 메서드 정의.
  • RangeableRepository 인터페이스 : 스프링 데이터 JPA Repository 인터페이스와 RangeableExecutor 인터페이스를 상속.
  • RangeableRepositoryImpl 클래스 : 스프링 데이터 JPA의 기본 구현체를 확장. RangeableRepository 인터페이스의 구현을 제공.

스프링 데이터 JPA에서 모든 리포지토리에 동일 기능을 추가하는 방법은 스프링 데이터 JPA 레퍼런스를 참고한다.

예제 코드 : https://github.com/madvirus/spring-data-jpa-rangeable

 

madvirus/spring-data-jpa-rangeable

init. Contribute to madvirus/spring-data-jpa-rangeable development by creating an account on GitHub.

github.com

Rangeable 클래스

import org.springframework.data.domain.Sort;

public class Rangeable {
    private int start;
    private int limit;
    private Sort sort;

    public Rangeable(int start, int limit, Sort sort) {
        this.start = start;
        this.limit = limit;
        this.sort = sort;
    }

    public int getStart() {
        return start;
    }

    public int getLimit() {
        return limit;
    }

    public Sort getSort() {
        return sort;
    }
}

* start : 시작행, limit : 개수, sort : 정렬

RangeableExecutor 인터페이스

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

import java.util.List;

public interface RangeableExecutor<T> {
    List<T> getRange(Specification<T> spec, Rangeable rangeable);
}

RangeableRepository 인터페이스

import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.Repository;

import java.io.Serializable;

@NoRepositoryBean
public interface RangeableRepository<T, ID extends Serializable>
        extends Repository<T, ID>, RangeableExecutor<T> {
}

RangeableRepositoryImpl 클래스

import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.io.Serializable;
import java.util.List;

public class RangeableRepositoryImpl<T, ID extends Serializable>
        extends SimpleJpaRepository<T, ID>
        implements RangeableRepository<T, ID> {

    public RangeableRepositoryImpl(
            JpaEntityInformation<T, ?> entityInformation, 
            EntityManager entityManager) {
        super(entityInformation, entityManager);
    }

    @Override
    public List<T> getRange(Specification<T> spec, Rangeable rangeable) {
        TypedQuery<T> query = getQuery(
                spec, getDomainClass(), rangeable.getSort());

        query.setFirstResult(rangeable.getStart());
        query.setMaxResults(rangeable.getLimit());

        return query.getResultList();
    }
}

* 기본 구현체인 SimpleJpaRepository 클래스를 확장해서 getRange() 구현

@EnableJpaRepositories로 기본 구현 지정

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@Configuration
@EnableJpaRepositories(repositoryBaseClass = RangeableRepositoryImpl.class)
public class SpringJpaConfiguration {
}

리포지토리에서 RangeableExecutor 인터페이스 사용

import org.springframework.data.repository.Repository;
import rangeable.jpa.RangeableExecutor;

public interface CommentRepository 
        extends Repository<Comment, Long>, RangeableExecutor<Comment> {
}

Rangeable로 일정 범위 조회

List<Comment> comments = repository.getRange(
        someSpec,
        new Rangeable(10, 5, Sort.by("id").descending()));

 

  1. 한입만 시즌2 2020.07.16 11:25

    잘 보고 갑니다~~

Spring Data Jpa의 Speicfication을 애용하는 편인데, 이 Specification을 사용해서 조건을 조합하다보면 다음과 같은 코드를 종종 작성하게 된다. (관련 내용은 http://javacan.tistory.com/entry/SpringDataJPA-Specifcation-Usage 참고)


Specifications<Check> specs = Specifications.where(

    CheckSpecs.yearQuarter(searchRequest.getYear(), searchRequest.getQuarter()));


if (searchRequest.hasTeamCd())

    specs = specs.and(CheckSpecs.teamCd(searchRequest.getTeamCd()));

if (searchRequest.hasPlanDate())

    specs = specs.and(CheckSpecs.planDate(searchRequest.getPlanDate()));


List<Check> checks = checkRepository.findAll(specs);


if 절과 각 Spec을 and로 엮는 코드가 실수하기 좋게 되어 있다. 이를 보완하고자 SpecBuilder라는 보조 클래스를 하나 만들었다. 이 클래스를 사용하면 위 코드를 다음과 같이 변경할 수 있다.


Specification<Check> spec = SpecBuilder.builder(Check.class)

        .and(CheckSpecs.yearQuarter(searchRequest.getYear(), searchRequest.getQuarter()))

        .whenHasText(searchRequest.getTeamCd(), str -> CheckSpec.teamCd(str))

        .whenHasText(searchRequest.getPlanDate(), CheckSpec::planDate)

        .toSpec();


.List<Check> checks = checkRepository.findAll(specs);


단순히 and로 조합하는 경우, if를 사용할 때보다 코드를 보기가 더 좋아졌다.


SpecBuilder의 완전한 코드는 다음과 같다.


public class SpecBuilder {


    public static <T> Builder<T> builder(Class<T> type) {

        return new Builder<T>();

    }


    public static class Builder<T> {

        private List<Specification<T>> specs = new ArrayList<>();


        public Builder<T> and(Specification<T> spec) {

            specs.add(spec);

            return this;

        }


        public Builder<T> whenHasText(String str, 

                 Function<String, Specification<T>> specSupplier) {

            if (StringUtils.hasText(str)) {

                specs.add(specSupplier.apply(str));

            }

            return this;

        }


        public Builder<T> when(String str, 

                 Function<String, Specification<T>> specSupplier) {

            specs.add(specSupplier.apply(str));

            return this;

        }

        

        public Builder<T> whenHasTextThenBetween(String from, String to,

                BiFunction<String, String, Specification<T>> specSupplier) {

            if (StringUtils.hasText(from) && StringUtils.hasText(to)) {

                specs.add(specSupplier.apply(from, to));

            }

            return this;

        }


        public Builder<T> whenIsTrue(Boolean cond,

                Supplier<Specification<T>> specSupplier) {

            if (cond != null && cond.booleanValue()) {

                specs.add(specSupplier.get());

            }

            return this;

        }


        public Specification<T> toSpec() {

            if (specs.isEmpty())

                return Specifications.where(null);

            else if (specs.size() == 1)

                return specs.get(0);

            else {

                return specs.stream().reduce(

                        Specifications.where(null),

                        (specs, spec) -> specs.and(spec),

                        (specs1, specs2) -> specs1.and(specs2));

            }

        }

    }

}


+ Recent posts