Overview:
In this tutorial, I would like to show you how we could achieve pagination for improved navigation of our application when we use Spring Reactive Data (R2DBC Pagination) with Spring WebFlux.
Spring Data R2DBC:
R2DBC stands for Reactive Relational DB connectivity.
Something like JPA (Java Persistence API), R2DBC is a specification for reactive drivers for relational DBs. As it is a separate specification, do not expect/compare with the features of JPA/Hibernate like @OneToMany, @ManyToMany etc.
You can take look at this article for a simple CRUD application with R2DBC.
R2DBC Pagination:
To show the pagination example with Spring WebFlux and R2DBC, I would like to take a simple application. As usual, lets consider a product-service. This service is responsible for maintaining all the products related information and might contain thousands of products.
When the user wants to view all the products, we might not want to load all the products info in 1 single page; instead we might show first few products and provide a navigation bar for the users to navigate as shown below.
Lets see how it works with Spring WebFlux and R2DBC.
Project Setup:
Go to spring initializer and create a Spring Boot project with below dependencies.
- Our Product table will be created as shown here. I place this sql file in this path – src/main/resources/init.sql.
create table product (
id bigint auto_increment,
description varchar(50),
price int,
primary key (id)
);
- Create a Product Entity as shown below.
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor(staticName = "create")
public class Product {
@Id
private Integer id;
private String description;
private Integer price;
}
- We do NOT have PagingAndSortingRepository in R2DBC. So create Product Repository as shown here.
- There is NO default findAll method which accepts Pageable. So we have to create one ourselves.
- Reactive repository does NOT return a Page<Product> or Mono<Page<Product>>. We have to get the Flux<Prpoduct> and convert to Page<Product>. We will do that in the service layer.
@Repository
public interface ProductRepository extends ReactiveSortingRepository<Product, Integer> {
Flux<Product> findAllBy(Pageable pageable);
}
Pagination Service:
In the service class, We send the PageRequest and get the Flux<Product>. We collect all the products for the page (this is NOT a blocking collect) and then we create the Page<Product> as shown below.
@Service
public class ProductService {
@Autowired
private ProductRepository repository;
public Mono<Page<Product>> getProducts(PageRequest pageRequest){
return this.repository.findAllBy(pageRequest)
.collectList()
.zipWith(this.repository.count())
.map(t -> new PageImpl<>(t.getT1(), pageRequest, t.getT2()));
}
}
WebFlux REST Controller:
Our REST controller will look like this.
@RestController
@RequestMapping("product")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("all")
public Mono<Page<Product>> getAll(@RequestParam("page") int page, @RequestParam("size") int size){
return this.productService.getProducts(PageRequest.of(page, size));
}
}
Data Setup Service:
Just for the demo purposes, I create another service class to insert some data in the H2 DB as and when we start our application. Here I insert 100 products when the app starts.
@Service
public class DataSetupService implements CommandLineRunner {
@Value("classpath:init.sql")
private Resource initSql;
@Autowired
private R2dbcEntityTemplate entityTemplate;
@Override
public void run(String... args) throws Exception {
String query = StreamUtils.copyToString(initSql.getInputStream(), StandardCharsets.UTF_8);
this.entityTemplate
.getDatabaseClient()
.sql(query)
.then()
.then(insertProducts())
.subscribe();
}
private Mono<Void> insertProducts(){
return Flux.range(1, 100)
.map(i -> Product.create(null, "product - " + i, ThreadLocalRandom.current().nextInt(1, 1000)))
.flatMap(this.entityTemplate::insert)
.doOnComplete(() -> System.out.println("Inserted all records"))
.then();
}
}
R2DBC Pagination – Demo:
- Request:
- I send the below request to load products.
http://localhost:8080/product/all?page=0&size=3
- Response:
{
"content": [
{
"id": 1,
"description": "product - 1",
"price": 1
},
{
"id": 2,
"description": "product - 2",
"price": 834
},
{
"id": 3,
"description": "product - 3",
"price": 665
}
],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"offset": 0,
"pageNumber": 0,
"pageSize": 3,
"paged": true,
"unpaged": false
},
"totalPages": 34,
"totalElements": 100,
"last": false,
"size": 3,
"number": 0,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"numberOfElements": 3,
"first": true,
"empty": false
}
Adjust the page and size to load the data with specific offset.
R2DBC Sorting:
Sorting the data is also very simple along with Pagination. For ex: To sort the products based on the price in descending order, we can do this way.
public Mono<Page<Product>> getProducts(PageRequest pageRequest){
return this.repository.findAllBy(pageRequest.withSort(Sort.by("price").descending()))
.collectList()
.zipWith(this.repository.count())
.map(t -> new PageImpl<>(t.getT1(), pageRequest, t.getT2()));
}
Summary:
We were able to successfully demonstrate the Spring Data R2DBC Pagination with Spring WebFlux.
The source code is available here.
Learn more about R2DBC and Spring WebFlux.
Happy coding 🙂