I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

In this article, we will mainly focus on implementing server side pagination in a Spring REST API and a simple AngularJS front-end.

We’ll also explore a commonly used table grid in Angular named UI Grid.

2. Dependencies

Here we detail various dependencies that are required for this article.

2.1. JavaScript

In order for Angular UI Grid to work, we will need the below scripts imported in our HTML.

2.2. Maven

For our backend we will be using Spring Boot, so we’ll need the below dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>

Note: Other dependencies were not specified here, for the full list, check the complete pom.xml in the GitHub project.

3. About the Application

The application is a simple student’s directory app which allows users to see the student details in a paginated table grid.

The application uses Spring Boot and runs in an embedded Tomcat server with an embedded database.

Finally, on the API side of things, there are a few ways to do pagination, described in the REST Pagination in Spring article here – which is highly recommended reading in conjunction with this article.

Our solution here is simple – having the paging information in a URI query as follows: /student/get?page=1&size=2.

4. The Client Side

First, we need to create the client-side logic.

4.1. The UI-Grid

Our index.html will have the imports we need and a simple implementation of the table grid:

<!DOCTYPE html>
<html lang="en" ng-app="app">
    <head>
        <link rel="stylesheet" href="https://cdn.rawgit.com/angular-ui/
          bower-ui-grid/master/ui-grid.min.css">
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/
          1.5.6/angular.min.js"></script>
        <script src="https://cdn.rawgit.com/angular-ui/bower-ui-grid/
          master/ui-grid.min.js"></script>
        <script src="view/app.js"></script>
    </head>
    <body>
        <div ng-controller="StudentCtrl as vm">
            <div ui-grid="gridOptions" class="grid" ui-grid-pagination>
            </div>
        </div>
    </body>
</html>

Let’s have a closer look at the code:

  • ng-app – is the Angular directive that loads the module app. All elements under these will be part of the app module
  • ng-controller – is the Angular directive that loads the controller StudentCtrl with an alias of vm. All elements under these will be part of the StudentCtrl controller
  • ui-grid – is the Angular directive that belongs to Angular ui-grid and uses gridOptions as its default settings, gridOptions is declared under $scope in app.js

4.2. The AngularJS Module

Let’s first define the module in app.js:

var app = angular.module('app', ['ui.grid','ui.grid.pagination']);

We declared the app module and we injected ui.grid to enable UI-Grid functionality; we also injected ui.grid.pagination to enable pagination support.

Next, we’ll define the controller:

app.controller('StudentCtrl', ['$scope','StudentService', 
    function ($scope, StudentService) {
        var paginationOptions = {
            pageNumber: 1,
            pageSize: 5,
        sort: null
        };

    StudentService.getStudents(
      paginationOptions.pageNumber,
      paginationOptions.pageSize).success(function(data){
        $scope.gridOptions.data = data.content;
        $scope.gridOptions.totalItems = data.totalElements;
      });

    $scope.gridOptions = {
        paginationPageSizes: [5, 10, 20],
        paginationPageSize: paginationOptions.pageSize,
        enableColumnMenus:false,
    useExternalPagination: true,
        columnDefs: [
           { name: 'id' },
           { name: 'name' },
           { name: 'gender' },
           { name: 'age' }
        ],
        onRegisterApi: function(gridApi) {
           $scope.gridApi = gridApi;
           gridApi.pagination.on.paginationChanged(
             $scope, 
             function (newPage, pageSize) {
               paginationOptions.pageNumber = newPage;
               paginationOptions.pageSize = pageSize;
               StudentService.getStudents(newPage,pageSize)
                 .success(function(data){
                   $scope.gridOptions.data = data.content;
                   $scope.gridOptions.totalItems = data.totalElements;
                 });
            });
        }
    };
}]);

Let’s now have a look at the custom pagination settings in $scope.gridOptions:

  • paginationPageSizes – defines the available page size options
  • paginationPageSize – defines the default page size
  • enableColumnMenus – is used to enable/disable the menu on columns
  • useExternalPagination – is required if you are paginating on the server side
  • columnDefs – the column names that will be automatically mapped to the JSON object returned from the server. The field names in the JSON Object returned from the server and the column name defined should match.
  • onRegisterApi – the ability to register public methods events inside the grid. Here we registered the gridApi.pagination.on.paginationChanged to tell UI-Grid to trigger this function whenever the page was changed.

And to send the request to the API:

app.service('StudentService',['$http', function ($http) {

    function getStudents(pageNumber,size) {
        pageNumber = pageNumber > 0?pageNumber - 1:0;
        return $http({
          method: 'GET',
            url: 'student/get?page='+pageNumber+'&size='+size
        });
    }
    return {
        getStudents: getStudents
    };
}]);

5. The Backend and the API

5.1. The RESTful Service

Here’s the simple RESTful API implementation with pagination support:

@RestController
public class StudentDirectoryRestController {

    @Autowired
    private StudentService service;

    @RequestMapping(
      value = "/student/get", 
      params = { "page", "size" }, 
      method = RequestMethod.GET
    )
    public Page<Student> findPaginated(
      @RequestParam("page") int page, @RequestParam("size") int size) {

        Page<Student> resultPage = service.findPaginated(page, size);
        if (page > resultPage.getTotalPages()) {
            throw new MyResourceNotFoundException();
        }

        return resultPage;
    }
}

The @RestController was introduced in Spring 4.0 as a convenience annotation which implicitly declares @Controller and @ResponseBody.

For our API, we declared it to accept two parameters which are page and size that would also determine the number of records to return to the client.

We also added a simple validation that will throw a MyResourceNotFoundException if the page number is higher than the total pages.

Finally, we’ll return Page as the Response – this is a super helpful component of Spring Data which has held pagination data.

5.2. The Service Implementation

Our service will simply return the records based on page and size provided by the controller:

@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentRepository dao;

    @Override
    public Page<Student> findPaginated(int page, int size) {
        return dao.findAll(new PageRequest(page, size));
    }
}

5.3. The Repository Implementation

For our persistence layer, we’re using an embedded database and Spring Data JPA.

First, we need to setup our persistence config:

@EnableJpaRepositories("org.baeldung.web.dao")
@ComponentScan(basePackages = { "org.baeldung.web" })
@EntityScan("org.baeldung.web.entity") 
@Configuration
public class PersistenceConfig {

    @Bean
    public JdbcTemplate getJdbcTemplate() {
        return new JdbcTemplate(dataSource());
    }

    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        EmbeddedDatabase db = builder
          .setType(EmbeddedDatabaseType.HSQL)
          .addScript("db/sql/data.sql")
          .build();
        return db;
    }
}

The persistence config is simple – we have @EnableJpaRepositories to scan the specified package and find our Spring Data JPA repository interfaces.

We have the @ComponentScan here to automatically scan for all beans and we have @EntityScan (from Spring Boot) to scan for entity classes.

We also declared our simple datasource – using an embedded database that will run the SQL script provided on startup.

Now it’s time we create our data repository:

public interface StudentRepository extends JpaRepository<Student, Long> {}

This is basically all that we need to do here; if you want to go deeper into how to set up and use the highly powerful Spring Data JPA, definitely read the guide to it here.

6. Pagination Request and Response

When calling the API – http://localhost:8080/student/get?page=1&size=5, the JSON response will look something like this:

{
    "content":[
        {"studentId":"1","name":"Bryan","gender":"Male","age":20},
        {"studentId":"2","name":"Ben","gender":"Male","age":22},
        {"studentId":"3","name":"Lisa","gender":"Female","age":24},
        {"studentId":"4","name":"Sarah","gender":"Female","age":26},
        {"studentId":"5","name":"Jay","gender":"Male","age":20}
    ],
    "last":false,
    "totalElements":20,
    "totalPages":4,
    "size":5,
    "number":0,
    "sort":null,
    "first":true,
    "numberOfElements":5
}

One thing to notice here is that server returns a org.springframework.data.domain.Page DTO, wrapping our Student Resources.

The Page object will have the following fields:

  • last – set to true if its the last page otherwise false
  • first – set to true if it’s the first page otherwise false
  • totalElements – the total number of rows/records. In our example, we passed this to the ui-grid options $scope.gridOptions.totalItems to determine how many pages will be available
  • totalPages – the total number of pages which was derived from (totalElements / size)
  • size – the number of records per page, this was passed from the client via param size
  • number – the page number sent by the client, in our response the number is 0 because in our backend we are using an array of Students which is a zero-based index, so in our backend, we decrement the page number by 1
  • sort – the sorting parameter for the page
  • numberOfElements – the number of rows/records return for the page

7. Testing Pagination

Let’s now set up a test for our pagination logic, using RestAssured; to learn more about RestAssured you can have a look at this tutorial.

7.1. Preparing the Test

For ease of development of our test class we will be adding the static imports:

io.restassured.RestAssured.*
io.restassured.matcher.RestAssuredMatchers.*
org.hamcrest.Matchers.*

Next, we’ll set up the Spring enabled test:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest("server.port:8888")

The @SpringApplicationConfiguration helps Spring know how to load the ApplicationContext, in this case, we used the Application.java to configure our ApplicationContext.

The @WebAppConfiguration was defined to tell Spring that the ApplicationContext to be loaded should be a WebApplicationContext.

And the @IntegrationTest was defined to trigger the application startup when running the test, this makes our REST services available for testing.

7.2. The Tests

Here is our first test case:

@Test
public void givenRequestForStudents_whenPageIsOne_expectContainsNames() {
    given().params("page", "0", "size", "2").get(ENDPOINT)
      .then()
      .assertThat().body("content.name", hasItems("Bryan", "Ben"));
}

This test case above is to test that when page 1 and size 2 is passed to the REST service the JSON content returned from the server should have the names Bryan and Ben.

Let’s dissect the test case:

  • given – the part of RestAssured and is used to start building the request, you can also use with()
  • get – the part of RestAssured and if used triggers a get request, use post() for post request
  • hasItems – the part of hamcrest that checks if the values have any match

We add a few more test cases:

@Test
public void givenRequestForStudents_whenResourcesAreRetrievedPaged_thenExpect200() {
    given().params("page", "0", "size", "2").get(ENDPOINT)
      .then()
      .statusCode(200);
}

This test asserts that when the point is actually called an OK response is received:

@Test
public void givenRequestForStudents_whenSizeIsTwo_expectNumberOfElementsTwo() {
    given().params("page", "0", "size", "2").get(ENDPOINT)
      .then()
      .assertThat().body("numberOfElements", equalTo(2));
}

This test asserts that when page size of two is requested the pages size that is returned is actually two:

@Test
public void givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources() {
    given().params("page", "0", "size", "2").get(ENDPOINT)
      .then()
      .assertThat().body("first", equalTo(true));
}

This test asserts that when the resources are called the first time the first page name value is true.

There are many more tests in the repository, so definitely have a look at the GitHub project.

8. Conclusion

This article illustrated how to implement a data table grid using UI-Grid in AngularJS and how to implement the required server side pagination.

The implementation of these examples and tests can be found in the GitHub project. This is a Maven project, so it should be easy to import and run as it is.

To run the Spring boot project, you can simply do mvn spring-boot:run and access it locally on http://localhost:8080/.

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE LESSONS

newest oldest most voted
Notify of
Melwin
Guest
Melwin

Great article! How would we implement the same if we are not using JPA and writing our own custom queries using jdbctemplate and row mappers to build our objects? Thanks!

Eugen Paraschiv
Guest

That’s an interesting question Melwin, and while it’s not particularly difficult, I’ve not covered it here on the site. It’s also somewhat dependent on the actual DB you’re using – which is why I wouldn’t necessarily do that with raw queries and would use something like QueryDSL or jOOQ.
Hope that points you in the right direction. Cheers,
Eugen.

Ferhat GHEBGHOUB
Guest
Ferhat GHEBGHOUB

Hello,
Thank you for this great article.
There is one thing to think about before implementing pagination. The “OrderBy”.
The used query must be ordered to avoid duplicates and not found elements. Be careful to not use the default JpaRepository.findAll() method. We have to specify an order (The ID field will be the best choice for efficient order).

Eugen Paraschiv
Guest

I agree Ferhat, there are many other concerns when retrieving data than just pagination.
This writeup is only focused on how to implement pagination, but of course that can and should be coupled with these other concerns. So, yes, you can have an order by, or a filter applied to the data set first – definitely.
Thanks,
Eugen.

Ferhat GHEBGHOUB
Guest
Ferhat GHEBGHOUB

Hello Eugen,
Sorry if I repeat, but I wanna just show an example to be clear and to complete my comment.
If we have data like : [1,2,3…20]
If we paginate without order, with page size 4, we can have this wrong result :
[1,4,12,15] [11,10,20,4] [7,8,1,17] [13,19,6,9] [2,3,16,8]
So we can observe that we have duplicated entries like : 4, 8
And dismissed entries like : 5, 14
We already faced it in production.

Thanks

Eugen Paraschiv
Guest

First, thanks for the example and for making sure the comment here is complete.
This is certainly interesting, because this is not what I had in mind. From what you’re saying here is sounds like this is an actual problem at the persistence level, not just a matter of filtering the results.
So, my question is – why would the ordering affect the correctness of the results?
Also, do you have an example I can have a look at?
Cheers,
Eugen.

Ferhat GHEBGHOUB
Guest
Ferhat GHEBGHOUB

Hy, The pagination is based on “ROW_NUM”. We already faced this problem in production, and I solved it. That why I’m certain 😉 So, without order, the same query with a clause “ROW_NUM between 5 and 8” will not always result the same rows. Instead of an ordered query. If we take my example, with pagination based on ROW_NUM, ordering ASC on id (we have only one column ‘id’), w’ll always have the same correct result: [1,2,3,4] [5,6,7,8] [9,10,11,12] … [17,18,19,20] You can try it with traced SQL queries. Because of DBMS caches and some other reasons, the problem will… Read more »

Ferhat GHEBGHOUB
Guest
Ferhat GHEBGHOUB

Hello Eugen,
The probleme is that pagination is translated to a query borned by a clause like “ROW_NUM between page_num and page_num+(page_size-1)”.
The same query borned this way will not always give the same result. Some times it will, because of DBMS caches and SQL execution plan.
To avoid this occasionnal error, we have to specify an order by with small double occurences. (Id is the best). I already solved it in production, that why I’m sure 😉
If we take my example (Simple table with just an ID), w’ll have a correct result oredering by id : [1,2,3,4][5,6,7,8]…[17,18,19,20].

Hope it help.
Cheers,
Ferhat.

Eugen Paraschiv
Guest

That makes perfect sense. You could potentially enforce an order internally, if no order is specified in the QL. I think that’s a better way to go than forcing the end-user to always specify an order.

Enhsuld Zorigtbal
Guest
Enhsuld Zorigtbal

Hello, github project?

Grzegorz Piwowarek
Guest
Grzegorz Piwowarek

Updated