package example.micronaut;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
interface SaasSubscriptionRepository extends CrudRepository<SaasSubscription, Long>, (1)
PagingAndSortingRepository<SaasSubscription, Long> { (2)
}
5. Paginated List - Spring Boot vs Micronaut Framework - Building a Rest API
This guide compares how to create a GET endpoint that returns a paginated JSON Array in Micronaut and Spring Boot applications.
Authors: Sergio del Amo
Micronaut Version: 4.6.3
1. Sample Project
You can download a sample application with the code examples in this article.
2. Introduction
This guide compares how to write a POST endpoint backed by a persistence layer in a Micronaut Framework and Spring Boot applications.
The Spring Boot application uses Spring Data and H2. The Micronaut application uses Micronaut Data and H2. Micronaut Data is a database access toolkit that uses Ahead of Time (AoT) compilation to pre-compute queries for repository interfaces that are then executed by a thin, lightweight runtime layer.
This guide is the fourth tutorial of Building a Rest API - a series of tutorials comparing how to develop a REST API with Micronaut Framework and Spring Boot.
3. Pagination
When returning multiple records you need some control over paging the data. Micronaut Data includes the ability to specify pagination requirements with the Pageable type. The same concepts exists in Spring Data with org.springframework.data.domain.Pageable.
4. Repository
Micronaut Data and Spring Data enhance the repository pattern by providing a paginated interfaces from which you can extend.
Spring Data package is org.springframework.data.repository
and Micronaut Data package is io.micronaut.data.repository
.
Spring Data | Micronaut Data | |
---|---|---|
Created, Read, Update, Delegate Operations |
|
|
Paginated Operations |
|
|
4.1. Spring Boot
The repository in the Spring Boot applications extends from both CrudRepository
and PagingAndSortingRepository
.
1 | By extending CrudRepository you enable automatic generation of CRUD (Create, Read, Update, Delete) operations. |
2 | Extend PagingAndSortingRepository to support paging and sorting. |
4.2. Micronaut
The repository in the Micronaut application extends from both PageableRepository
.
package example.micronaut;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.PageableRepository;
@JdbcRepository(dialect = Dialect.H2) (1)
interface SaasSubscriptionRepository extends PageableRepository<SaasSubscription, Long> { (2)
}
1 | @JdbcRepository with a specific dialect. |
2 | PageableRepository extends CrudRepository , which provides automatic generation of CRUD (Create, Read, Update, Delete) operations and adds methods for pagination. |
5. Controller
Both frameworks simplify the creation of paginated endpoints by allowing the binding of Pageable
as a controller method parameter.
5.1. Spring Boot Controller
In the Spring Boot application, we build a PageRequest
, a basic Java Bean implementation of Pageable, with the Pageable
parameter and then pass it to the repository.
package example.micronaut;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController (1)
@RequestMapping("/subscriptions") (2)
public class SaasSubscriptionGetListController {
private static final Sort DEFAULT_SORT = Sort.by(Sort.Direction.ASC, "cents");
private final SaasSubscriptionRepository repository;
private SaasSubscriptionGetListController(SaasSubscriptionRepository repository) { (3)
this.repository = repository;
}
@GetMapping (4)
private ResponseEntity<List<SaasSubscription>> findAll(Pageable pageable) {
PageRequest pageRequest = pageRequestOf(pageable);
Page<SaasSubscription> page = repository.findAll(pageRequest);
return ResponseEntity.ok(page.getContent());
}
private static PageRequest pageRequestOf(Pageable pageable) {
return PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
pageable.getSortOr(DEFAULT_SORT)
);
}
}
1 | Annotate a class @RestController to identify the class as a Component capable of handling HTTP requests. |
2 | @RequestMapping identifies the request paths that invoke this Controller. |
3 | Use constructor injection to inject a bean of type SaasSubscriptionRepository . |
4 | The @GetMapping annotation maps the findAll method to an HTTP GET request on /subscriptions . |
5.2. Micronaut Controller
In the Micronaut application, we pass a Pageable
instance to the repository.
package example.micronaut;
import io.micronaut.data.model.Page;
import io.micronaut.data.model.Pageable;
import io.micronaut.data.model.Sort;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
@Controller("/subscriptions") (1)
class SaasSubscriptionGetListController {
private static final Sort CENTS = Sort.of(Sort.Order.asc("cents"));
private final SaasSubscriptionRepository repository;
SaasSubscriptionGetListController(SaasSubscriptionRepository repository) { (2)
this.repository = repository;
}
@Get (3)
Iterable<SaasSubscription> findAll(Pageable pageable) { (4)
Page<SaasSubscription> page = repository.findAll(pageable.getSort().isSorted()
? pageable
: Pageable.from(pageable.getNumber(), pageable.getSize(), CENTS)
); (5)
return page.getContent();
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /subscriptions . |
2 | Use constructor injection to inject a bean of type SaasSubscriptionRepository . |
3 | The @Get annotation maps the findAll method to an HTTP GET request on /subscriptions . |
4 | Pageable includes the ability to specify pagination requirements. You can bind it as a controller method parameter. |
5 | PageableRepository provides a findAll method which takes a Pageable parameter. |
6. Tests
In this tutorial, we use AssertJ in the tests. Moreover, we use Jayway JsonPath - a Java DSL for reading JSON documents.
In this tutorial, we use Json-smart in the tests.
Json-smart is a performance focused, JSON processor lib.
6.1. See Data
The seed data used in the tests is the same for both applications. We modified it to contain multiple entries.
INSERT INTO saas_subscription(id, name, cents) VALUES (99, 'Advanced', 2900);
INSERT INTO saas_subscription(id, name, cents) VALUES (100, 'Essential', 1400);
INSERT INTO saas_subscription(id, name, cents) VALUES (101, 'Professional', 4900);
6.2. Spring Boot Test
The following tests verify the following scenarios:
-
Returning a list specifying the sorting
-
Returning a list with the default sorting
-
Returning a list specifying no pageable parameters
package example.micronaut;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import net.minidev.json.JSONArray;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) (1)
class SaasSubscriptionGetListControllerTest {
@Autowired (2)
TestRestTemplate restTemplate; (3)
@Test
void shouldReturnASortedPageOfSaasSubscriptions() {
ResponseEntity<String> response = restTemplate.getForEntity("/subscriptions?page=0&size=1&sort=cents,desc", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
DocumentContext documentContext = JsonPath.parse(response.getBody());
JSONArray read = documentContext.read("$[*]");
assertThat(read.size()).isEqualTo(1);
Integer cents = documentContext.read("$[0].cents");
assertThat(cents).isEqualTo(4900);
response = restTemplate.getForEntity("/subscriptions?page=0&size=1", String.class);
documentContext = JsonPath.parse(response.getBody());
cents = documentContext.read("$[0].cents");
assertThat(cents).isEqualTo(1400);
}
@Test
void shouldReturnAPageOfSaasSubscriptions() {
ResponseEntity<String> response = restTemplate.getForEntity("/subscriptions?page=0&size=1", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
DocumentContext documentContext = JsonPath.parse(response.getBody());
JSONArray page = documentContext.read("$[*]");
assertThat(page.size()).isEqualTo(1);
}
@Test
void shouldReturnAllSaasSubscriptionsWhenListIsRequested() {
ResponseEntity<String> response = restTemplate.getForEntity("/subscriptions", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
DocumentContext documentContext = JsonPath.parse(response.getBody());
int saasSubscriptionCount = documentContext.read("$.length()");
assertThat(saasSubscriptionCount).isEqualTo(3);
JSONArray ids = documentContext.read("$..id");
assertThat(ids).containsExactlyInAnyOrder(99, 100, 101);
JSONArray cents = documentContext.read("$..cents");
assertThat(cents).containsExactlyInAnyOrder(1400, 2900, 4900);
}
}
1 | The @SpringBootTest annotation tells Spring Boot to look for a main configuration class (one with @SpringBootApplication , for instance) and use that to start a Spring application context. |
2 | Inject a bean of type TestRestTemplate by using @Autowired on the field definition. |
3 | TestRestTemplate is a helper to ease to execution of HTTP requests against the locally running application. |
6.3. Micronaut Test
The Micronaut Test is almost identical to the previous Spring Boot test. The main difference is that the Micronaut Test uses the built-in HttpClient
instead of TestRestTemplate
and HttpResponse
instead of ResponseEntity
.
package example.micronaut;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.uri.UriBuilder;
import io.micronaut.test.annotation.Sql;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import net.minidev.json.JSONArray;
import org.junit.jupiter.api.Test;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
@Sql(value = {"classpath:schema.sql", "classpath:data.sql"}) (1)
@MicronautTest (2)
class SaasSubscriptionGetListControllerTest {
@Inject
@Client("/")
HttpClient httpClient; (3)
@Test
void shouldReturnASortedPageOfSaasSubscriptions() {
BlockingHttpClient client = httpClient.toBlocking();
URI uri = UriBuilder.of("/subscriptions")
.queryParam("page", 0)
.queryParam("size", 1)
.queryParam("sort", "cents,desc")
.build();
HttpResponse<String> response = client.exchange(HttpRequest.GET(uri), String.class);
assertThat(response.status().getCode()).isEqualTo(HttpStatus.OK.getCode());
DocumentContext documentContext = JsonPath.parse(response.body());
JSONArray page = documentContext.read("$[*]");
assertThat(page).hasSize(1);
Integer cents = documentContext.read("$[0].cents");
assertThat(cents).isEqualTo(4900);
}
@Test
void shouldReturnAPageOfSaasSubscriptions() {
BlockingHttpClient client = httpClient.toBlocking();
URI uri = UriBuilder.of("/subscriptions")
.queryParam("page", 0)
.queryParam("size", 1)
.build();
HttpResponse<String> response = client.exchange(HttpRequest.GET(uri), String.class);
assertThat(response.status().getCode()).isEqualTo(HttpStatus.OK.getCode());
DocumentContext documentContext = JsonPath.parse(response.body());
JSONArray page = documentContext.read("$[*]");
assertThat(page.size()).isEqualTo(1);
}
@Test
void shouldReturnAllSaasSubscriptionsWhenListIsRequested() {
BlockingHttpClient client = httpClient.toBlocking();
HttpResponse<String> response = client.exchange("/subscriptions", String.class);
assertThat(response.status().getCode()).isEqualTo(HttpStatus.OK.getCode());
DocumentContext documentContext = JsonPath.parse(response.body());
int saasSubscriptionCount = documentContext.read("$.length()");
assertThat(saasSubscriptionCount).isEqualTo(3);
JSONArray ids = documentContext.read("$..id");
assertThat(ids).containsExactlyInAnyOrder(99, 100, 101);
JSONArray cents = documentContext.read("$..cents");
assertThat(cents).containsExactlyInAnyOrder(1400, 2900, 4900);
}
}
1 | @Sql allows you to load SQL before tests |
2 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info. |
3 | Inject the HttpClient bean and point it to the embedded server. |
7. Conclusion
Adding pagination to the persistence layer is easy in both frameworks, and the API is almost identical. However, Micronaut Data’s compile-time/reflection-free approach results in better performance, smaller stack traces, and reduced memory consumption.
8. License
All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…). |