Angular datatables with server side pagination using Spring Data

Posted at — May 5, 2016
Riekpil logo
Learn how to test real-world applications with the Testing Spring Boot Applications Masterclass. Comprehensive online course with 8 modules and 130+ video lessons to master well-known Java testing libraries: JUnit 5, Mockito, Testcontainers, WireMock, Awaitility, Selenium, LocalStack, Selenide, and Spring's Outstanding Test Support.

We are using angular-datatables in a project. So far, we just returned all entities from the server’s REST controller (Using Spring Boot and Spring Data on the server side). I wanted to see how I could implement server side pagination to avoid returning all records at once.

I was lucky to find spring-data-jpa-datatables which makes it very easy to do.

First, add the dependency in your pom.xml:

<dependency>
    <groupId>com.github.darrachequesne</groupId>
    <artifactId>spring-data-jpa-datatables</artifactId>
    <version>2.1</version>
</dependency>

In my UserController, I have currently this:

@RequestMapping(method = RequestMethod.GET)
@Secured(Roles.ADMIN)
public List<UserDto> getUsers() {
    return StreamSupport.stream(userRepository.findAll().spliterator(), false)
                        .map(UserDto::fromUser)
                        .collect(Collectors.toList());
}

I added a new method that would be used for the server side pagination and searching (I could have replaced the old method as well, as it will no longer be used):

@RequestMapping(value = "/datatables-view", method = RequestMethod.POST)
@Secured(Roles.ADMIN)
public DataTablesOutput<UserDto> getUsersForDatatables(@Valid @RequestBody DataTablesInput input) {
    DataTablesOutput<User> usersTest = userRepository.findAll(input);

    if (usersTest.getError() != null) {
        throw new IllegalArgumentException(usersTest.getError());
    }

    DataTablesOutput<UserDto> result = new DataTablesOutput<>();
    result.setData(usersTest.getData().stream().map(UserDto::fromUser).collect(Collectors.toList()));
    result.setDraw(usersTest.getDraw());
    result.setError(usersTest.getError());
    result.setRecordsFiltered(usersTest.getRecordsFiltered());
    result.setRecordsTotal(usersTest.getRecordsTotal());
    return result;
}

To support this controller, you need to update the UserRepository to extend from DataTablesRepository, so change this:

public interface UserRepository extends CrudRepository<User, UserId>, UserRepositoryCustom {

to

public interface UserRepository extends DataTablesRepository<User, UserId>, UserRepositoryCustom {

On the client side, I had this code:

$scope.dtOptions = DTOptionsBuilder.fromFnPromise(function () {
    return Users.query().$promise;
})

    .withBootstrap()
    .withPaginationType('simple_numbers')
    .withDisplayLength(20)
    .withOption('createdRow', function (row) {
        // Recompiling so we can bind Angular directive to the DT
        $compile(angular.element(row).contents())($scope);
    })
    .withOption('saveState', true)
    .withOption('order', [0, 'asc']);

    // Datatables columns builder

    $scope.dtColumns = [..
        ] //column definitions here

This code now changes to:

$scope.dtOptions = DTOptionsBuilder.newOptions()
.withOption('ajax', {
    contentType: 'application/json',
    url: '/api/users/datatables-view',
    type: 'POST',
    beforeSend: function(xhr) {
        xhr.setRequestHeader("Authorization",
        "Bearer " + AuthenticationService.getAccessToken());
},

data: function(data, dtInstance) {
    // The returned object has 'email' as property, but the server entity has 'emailAddress'
    // We need to override what we ask to the server here otherwise search will not work
    data.columns[1].data = "emailAddress";

    // Any values you set on the data object will be passed along as parameters to the server
    //data.access_token = AuthenticationService.getAccessToken();
    return JSON.stringify(data);
}
})
.withDataProp('data') // This is the name of the value in the returned recordset which contains the actual data
.withOption('serverSide', true)
.withBootstrap()
.withPaginationType('simple_numbers')
.withDisplayLength(20)
.withOption('createdRow', function (row) {
    // Recompiling so we can bind Angular directive to the DT
    $compile(angular.element(row).contents())($scope);
})

.withOption('saveState', true)
.withOption('order', [0, 'asc']);

The most important things are:

  • Set the contentType so that we send JSON to the REST controller

  • Set the url that points to our new controller method

  • Set the type to POST since we accept a POST in the controller

  • Add a beforeSend function to set the Authorization header so we can access the controller method that is secured with Spring Security

  • Add a data function to return the data object as JSON

One thing I had to do additionally is this line:

data.columns[1].data = "emailAddress";

The reason for this is that I return UserDto objects from the controller which have email as a property and thus the columns are defined like that in JavaScript. However, the real User entity on the server uses emailAddress as property. With this line, the server side code will use the good property for searching and sorting.

After all this, you can check in the developer console to the requests and responses, only the actual needed data will be returned from the server. The search box will work and also the sorting will work.

What is also very nice is that the pagination adapts perfectly. And when you start to search, it also shows this in the footer:

Showing 1 to 9 of 9 entries (filtered from 24 total entries)

And that is all you need to get pagination and sorting with server-side processing to handle large data sets using AngularJS, Datatables and Spring.

This know-how originated during the development of a PegusApps project.

If you want to be notified in the future about new articles, as well as other interesting things I'm working on, join my mailing list!
I send emails quite infrequently, and will never share your email address with anyone else.