ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • R2dbcEntityTemplate
    Spring Reactive Web Application/Spring WebFlux 2023. 9. 10. 09:00
    반응형

      Spring Data 패밀리 프로젝트에서 데이터베이스에 액세스하기 위해 Repository를 사용하는 방식은 널리 알려진 방법입니다.

     

      Spring Data R2DBC는 Repository를 사용한 데이터 액세스 방식뿐만 아니라 가독성 좋은 SQL 쿼리문을 작성하는 것과 같은 자연스러운 방식으로 메서드를 조합하여 데이터베이스와 인터랙션할 수 있는 R2dbcEntityTemplate을 제공합니다.

     

    Note. 템플릿/콜백 패턴이 적용된 JdbcTemplate처럼 R2dbcEntityTemplate 역시 템플릿을 사용합니다. 단, R2dbcEntityTemplate은 JPA 기술에 사용되는 Query DSL과 유사한 방식의 Query 생성 메서드의 조합과 Entity 객체를 템플릿에 전달하여 데이터베이스와 인터랙션합니다.

     

    R2dbcEntityTemplate을 사용한 서비스 클래스

      R2dbcEntityTemplate은 SQL 쿼리문의 시작 구문인 SELECT, INSERT, UPDATE, DELETE 등에 해당하는 select(), insert(), update(), delete() 메서드를 Entrypoint method라고 부르며, all(), count(), one() 등의 메서드처럼 SQL문을 생성하고 최종적으로 SQL 문을 실행하는 메서드를 Terminating method라고 부릅니다.

    import com.itvillage.exception.BusinessLogicException;
    import com.itvillage.exception.ExceptionCode;
    import com.itvillage.utils.CustomBeanUtils;
    import lombok.NonNull;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
    import org.springframework.stereotype.Service;
    import reactor.core.publisher.Mono;
    
    import java.util.List;
    
    import static org.springframework.data.relational.core.query.Criteria.where;
    import static org.springframework.data.relational.core.query.Query.query;
    
    @Slf4j
    @Service("bookServiceV6")
    @RequiredArgsConstructor
    public class BookService {
        private final @NonNull R2dbcEntityTemplate template;
        private final @NonNull CustomBeanUtils<Book> beanUtils;
    
        public Mono<Book> saveBook(Book book) {
            return verifyExistIsbn(book.getIsbn())
                    .then(template.insert(book));
        }
    
        public Mono<Book> updateBook(Book book) {
            return findVerifiedBook(book.getBookId())
                    .map(findBook -> beanUtils.copyNonNullProperties(book, findBook))
                    .flatMap(updatingBook -> template.update(updatingBook));
        }
    
        public Mono<Book> findBook(long bookId) {
            return findVerifiedBook(bookId);
        }
    
        public Mono<List<Book>> findBooks() {
            return template.select(Book.class).all().collectList();
        }
    
        private Mono<Void> verifyExistIsbn(String isbn) {
            return template.selectOne(query(where("ISBN").is(isbn)), Book.class)
                    .flatMap(findBook -> {
                        if (findBook != null) {
                            return Mono.error(new BusinessLogicException(
                                    ExceptionCode.BOOK_EXISTS));
                        }
                        return Mono.empty();
                    });
        }
    
        private Mono<Book> findVerifiedBook(long bookId) {
            return template.selectOne(query(where("BOOK_ID").is(bookId))
                                                                            , Book.class)
                    .switchIfEmpty(Mono.error(new BusinessLogicException(
                                                    ExceptionCode.BOOK_NOT_FOUND)));
        }
    }

     

    Terminating method

      R2dbcEntityTemplate은 select() 메서드와 함께 사용할 수 있는 Terminating method를 다음 표와 같이 지원합니다.

    Terminating method 설명
    first() 조건에 일치하는 result row 중 first row를 얻고자 할 경우 사용할 수 있습니다. 만약 조건에 일치하는 row가 없다면 Mono<Void>를 리턴합니다.
    one() 조건에 일치하는 result row가 단 하나일 경우 사용할 수 있습니다. 만약 조건에 일치하는 row가 없다면 Mono<Void>를 리턴하며, result row가 한 건보다 많을 경우 Exception이 발생합니다.
    all() 조건에 일치하는 모든 result row를 얻고자 할 경우 사용할 수 있습니다.
    count() 조건에 일치하는 데이터의 건수만 조회할 경우 사용할 수 있습니다. 리턴 타입은 Mono<Long> 입니다.
    exists() 조건에 일치하는 result row가 존재하는지 여부를 확인하고자 할 경우 사용할 수 있습니다. 리턴 타입은 Mono<Boolean> 입니다.

     

    Criteria method

      R2dbcEntityTemplate은 SQL 연산자에 해당하는 다양한 Criteria method를 다음 표와 같이 지원합니다.

    Criteria method 설명
    and(String column) SQL 쿼리문에서 'and' 연산자에 해당됩니다.
    or(String column) SQL 쿼리문에서 'or' 연산자에 해당됩니다.
    greaterThan(Object o) SQL 쿼리문에서 '>' 연산자에 해당됩니다.
    greaterThanOrEquals(Object o) SQL 쿼리문에서 '>=' 연산자에 해당됩니다.
    in(Object... o) 또는 in(Collection<?> collection) SQL 쿼리문에서 'IN' 연산자에 해당됩니다.
    is(Object o) SQL 쿼리문에서 '=' 연산자에 해당됩니다.
    isNull() SQL 쿼리문에서 'IS NULL' 연산자에 해당됩니다.
    inNotNull() SQL 쿼리문에서 'IS NOT NULL' 연산자에 해당됩니다.
    lessThan(Object o) SQL 쿼리문에서 '<' 연산자에 해당됩니다.
    lessThanOrEquals(Object o) SQL 쿼리문에서 '<=' 연산자에 해당됩니다.
    like(Object o) SQL 쿼리문에서 'LIKE' 연산자에 해당됩니다.
    not(Object o) SQL 쿼리문에서 '!=' 또는 'NOT' 연산자에 해당됩니다.
    notIn(Object... o) 또는 notIn(Collection<?> collection) SQL 쿼리문에서 'NOT IN' 연산자에 해당됩니다.

     

    Pagination

      R2dbcEntityTemplate은 limit(), offset(), sort() 등의 쿼리 빌드 메서드를 조합하면 페이지네이션 처리를 간단히 적용할 수 있습니다.

     

      다음은 쿼리 빌드 메서드 대신 Reactor의 Operator 체인을 이용해서 직접 페이지네이션 처리를 적용한 코드입니다.

    import com.itvillage.exception.BusinessLogicException;
    import com.itvillage.exception.ExceptionCode;
    import com.itvillage.utils.CustomBeanUtils;
    import lombok.NonNull;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
    import org.springframework.stereotype.Service;
    import org.springframework.validation.annotation.Validated;
    import reactor.core.publisher.Mono;
    import reactor.util.function.Tuple2;
    import reactor.util.function.Tuples;
    
    import javax.validation.constraints.Positive;
    import java.util.List;
    
    import static org.springframework.data.relational.core.query.Criteria.where;
    import static org.springframework.data.relational.core.query.Query.query;
    
    /**
     * 페이지네이션 적용
     */
    @Slf4j
    @Validated
    @Service("bookServiceV8")
    @RequiredArgsConstructor
    public class BookService {
        private final @NonNull R2dbcEntityTemplate template;
        private final @NonNull CustomBeanUtils<Book> beanUtils;
    
        ...
    
        public Mono<List<Book>> findBooks(@Positive long page, @Positive long size) {
    
            return template
                    .select(Book.class)
                    .count()
                    .flatMap(total -> {
                        Tuple2<Long, Long> skipAndTake = getSkipAndTake(total, page, size);
                        return template
                                .select(Book.class)
                                .all()
                                .skip(skipAndTake.getT1())
                                .take(skipAndTake.getT2())
                                .collectSortedList((Book b1, Book b2) ->
                                        (int) (b2.getBookId() - b1.getBookId()));
                    });
        }
    
        ...
    
        private Tuple2<Long, Long> getSkipAndTake(long total, long movePage, long size) {
            long totalPages = (long) Math.ceil((double) total / size);
            long page = movePage > totalPages ? totalPages : movePage;
            long skip = total - (page * size) < 0 ? 0 : total - (page * size);
            long take = total - (page * size) < 0 ? total - ((page - 1) * size) : size;
    
            return Tuples.of(skip, take);
        }
    }
    • count() Opreator로 저장된 도서의 총 개수를 구한 후 flatMap() Operator 내부에서 페이지네이션 처리를 수행합니다.
    • skip() Operator는 페이지의 시작 시점으로 이동하기 위해 페이지 수만큼 emit된 데이터를 건너뛰는 역할을 수행합니다.
    • take() Operator는 페이지의 데이터 개수(size)만큼 데이터를 가져오는 역할을 수행합니다.
    • getSkipAndTake(total, page, size) 메서드는 데이터의 총 개수로 전체 페이지 수를 구하고, 이동할 페이지의 데이터 시작 지점 전까지 건너뛸 데이터 개수와 가져올 데이터 개수를 계산합니다.

     

     

     

     
    반응형

    'Spring Reactive Web Application > Spring WebFlux' 카테고리의 다른 글

    WebClient  (0) 2023.09.16
    예외 처리  (0) 2023.09.16
    Spring Data R2DBC  (0) 2023.09.09
    함수형 엔드포인트(Functional Endpoint)  (0) 2023.08.26
    애너테이션 기반 컨트롤러(Annotated Controller)  (0) 2023.08.26

    댓글

Designed by Tistory.