Search code examples
paginationcontrollermicronaut

Micronaut controller with pagination using Pageable


I am trying to use a Micronaut controller with pagination. Micronaut-Data has this Spring inspired way to access the repositories using the Pageable class and returning a Page

The problem comes when you want to show this paginated data. I have not been able create a call the controller with pagination. Here I have a simple controller:

    @Controller
    public class PageableController {

        private static final Logger LOGGER = LoggerFactory.getLogger(PageableController.class);

        @Get(produces = APPLICATION_JSON, value = "/test{?pageable}")
        public Page<String> getNames(@Nullable Pageable pageable) {

            LOGGER.info("pageable {}", pageable);
            if( pageable == null){
                return Page.of(Arrays.asList("foo", "bar"), Pageable.UNPAGED, 2);
            }else{
                return Page.of(Arrays.asList("foo", "bar"), pageable, 2);
            }
        }
    }

I would expect to be able to call it with something like this. But currently the logger shows that pageable is always null:

    @MicronautTest
    class PageableControllerTest {

        @Inject
        @Client("/")
        private RxHttpClient client;

        @Test
        void callsWithPageable() {
            String uri = "/test?size=20&number=2";
            String orders = client.toBlocking().retrieve(HttpRequest.GET(uri));

            //TODO, assert orders and pagination
        }

It would be even better if we could test it with something like:

    @Test
    void callsWithPageableParsingJson() {
        String uri = "/test?size=20&number=2";

        //This fails to parse as it can't build pages.
        Page<String> pages = client.toBlocking().retrieve(HttpRequest.GET(uri), pageOf(String.class));

        assertThat(pages.getSize(),   is(2));
        assertThat(pages.getContent(), contains("foo", "bar"));
    }

    // Inspired by Argument.listOf
    private static <T> Argument<Page<T>> pageOf(Class<T> type) {
        return Argument.of((Class<Page<T>>) ((Class) Page.class), type);
    }

And this Micronaut bug shows that the right way to paginate is with Micronaut Data


Solution

  • The problem was solved by adding the following dependecy:

        <dependency>
           <groupId>io.micronaut.data</groupId>
           <artifactId>micronaut-data-runtime</artifactId>
           <version>1.0.0.M1</version>
        </dependency>
    

    My controller layer had access to the micronaut-data-model but this jar contains the important class PageableRequestArgumentBinder. Just by being i the classpath it will automatically be injected as a binder with no need for extra configuration.

    And yes, Free See was right and now I can remove the pageable argument from the path and the argument from the method does not need to be @Nullable :

        @Controller
        public class PageableController {
    
            private static final Logger LOGGER = LoggerFactory.getLogger(PageableController.class);
    
            @Get(produces = APPLICATION_JSON, value = "/test")
            public Page<String> getNames(Pageable pageable) {
    
                LOGGER.info("pageable {}", pageable);
                return Page.of(Arrays.asList("foo", "bar", "baz"), pageable, 3);
            }
    

    To call it we new to use the standard parameters names defined in DataConfiguration.PageableConfiguration.

    • DEFAULT_PAGE_PARAMETER "page"
    • DEFAULT_SIZE_PARAMETER "size"
    • DEFAULT_SORT_PARAMETER "sort"

    If you want to use different parameters you can change it with properties:

    micronaut:
      data:
        pageable:
         max-page-size: 500
    

    And you can test it with

        @Test
        void callsWithPageable() {
            String uri = "/test?page=1&size=2";
    
            Page<String> pages = client.toBlocking().retrieve(HttpRequest.GET(uri), pageOf(String.class));
    
            assertThat(pages.getPageNumber(),   is(1));
            assertThat(pages.getTotalPages(),   is(2));
            assertThat(pages.getSize(),   is(2));
            assertThat(pages.getContent(), contains("foo", "bar", "baz"));
        }
    

    And, to make things even better, the client can convert the result to a page using the pageOf method that returns Argument>