Pagination avec Spring REST et table AngularJS

Haut REST

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS

1. Vue d'ensemble

Dans cet article, nous nous concentrerons principalement sur la mise en œuvre de la pagination côté serveur dans une API Spring REST et un simple front-end AngularJS.

Nous explorerons également une grille de table couramment utilisée dans Angular nommée UI Grid.

2. Dépendances

Nous détaillons ici différentes dépendances requises pour cet article.

2.1. JavaScript

Pour que Angular UI Grid fonctionne, nous aurons besoin des scripts ci-dessous importés dans notre HTML.

  • Angulaire JS (1.5.8)
  • Grille d'interface utilisateur angulaire

2.2. Maven

Pour notre backend, nous utiliserons Spring Boot , nous aurons donc besoin des dépendances ci-dessous:

 org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-tomcat provided 

Remarque: les autres dépendances n'ont pas été spécifiées ici, pour la liste complète, vérifiez le pom.xml complet dans le projet GitHub.

3. À propos de l'application

L'application est une simple application d'annuaire d'étudiant qui permet aux utilisateurs de voir les détails de l'étudiant dans une grille de tableau paginée.

L'application utilise Spring Boot et s'exécute dans un serveur Tomcat intégré avec une base de données intégrée.

Enfin, du côté de l'API, il existe plusieurs façons de faire la pagination, décrites dans l'article REST Pagination in Spring ici - qui est fortement recommandée à lire conjointement avec cet article.

Notre solution ici est simple - avoir les informations de pagination dans une requête URI comme suit: / student / get? Page = 1 & size = 2 .

4. Côté client

Tout d'abord, nous devons créer la logique côté client.

4.1. Le UI-Grid

Notre index.html aura les importations dont nous avons besoin et une implémentation simple de la grille de table:

Regardons de plus près le code:

  • ng-app - est la directive Angular qui charge l' application du module . Tous les éléments sous ceux-ci feront partie du module d' application
  • ng-controller - est la directive Angular qui charge le contrôleur StudentCtrl avec un alias de vm. Tous les éléments sous ceux-ci feront partie du contrôleur StudentCtrl
  • ui-grid - est la directive Angular qui appartient à Angular ui-grid et utilise gridOptions comme paramètres par défaut, gridOptions est déclaré sous $ scope dans app.js

4.2. Le module AngularJS

Définissons d'abord le module dans app.js :

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

Nous avons déclaré le module d' application et nous avons injecté ui.grid pour activer la fonctionnalité UI-Grid; nous avons également injecté ui.grid.pagination pour permettre la prise en charge de la pagination.

Ensuite, nous définirons le contrôleur:

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; }); }); } }; }]); 

Jetons maintenant un œil aux paramètres de pagination personnalisés dans $ scope.gridOptions :

  • paginationPageSizes - définit les options de taille de page disponibles
  • paginationPageSize - définit la taille de page par défaut
  • enableColumnMenus - est utilisé pour activer / désactiver le menu sur les colonnes
  • useExternalPagination - est requis si vous paginez côté serveur
  • columnDefs - les noms de colonne qui seront automatiquement mappés à l'objet JSON renvoyé par le serveur. Les noms de champ dans l'objet JSON renvoyés par le serveur et le nom de colonne défini doivent correspondre.
  • onRegisterApi - la possibilité d'enregistrer des événements de méthodes publiques dans la grille. Ici, nous avons enregistré le gridApi.pagination.on.paginationChanged pour indiquer à UI-Grid de déclencher cette fonction chaque fois que la page a été modifiée.

Et pour envoyer la requête à l'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. Le backend et l'API

5.1. Le service RESTful

Voici l'implémentation simple de l'API RESTful avec prise en charge de la pagination:

@RestController public class StudentDirectoryRestController { @Autowired private StudentService service; @RequestMapping( value = "/student/get", params = { "page", "size" }, method = RequestMethod.GET ) public Page findPaginated( @RequestParam("page") int page, @RequestParam("size") int size) { Page resultPage = service.findPaginated(page, size); if (page > resultPage.getTotalPages()) { throw new MyResourceNotFoundException(); } return resultPage; } }

Le @RestController a été introduit dans Spring 4.0 en tant qu'annotation pratique qui déclare implicitement @Controller et @ResponseBody.

Pour notre API, nous l'avons déclaré accepter deux paramètres qui sont la page et la taille qui détermineraient également le nombre d'enregistrements à renvoyer au client.

Nous avons également ajouté une simple validation qui lèvera une MyResourceNotFoundException si le numéro de page est supérieur au nombre total de pages.

Enfin, nous retournerons Page comme réponse - c'est un composant très utile de S pring Data qui contient des données de pagination.

5.2. La mise en œuvre du service

Notre service renverra simplement les enregistrements en fonction de la page et de la taille fournies par le contrôleur:

@Service public class StudentServiceImpl implements StudentService { @Autowired private StudentRepository dao; @Override public Page findPaginated(int page, int size) { return dao.findAll(new PageRequest(page, size)); } } 

5.3. L'implémentation du référentiel

Pour notre couche de persistance, nous utilisons une base de données intégrée et Spring Data JPA.

Tout d'abord, nous devons configurer notre configuration de persistance:

@EnableJpaRepositories("com.baeldung.web.dao") @ComponentScan(basePackages = { "com.baeldung.web" }) @EntityScan("com.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; } } 

La configuration de la persistance est simple - nous avons @EnableJpaRepositories pour analyser le package spécifié et trouver nos interfaces de référentiel Spring Data JPA.

Nous avons le @ComponentScan ici pour rechercher automatiquement tous les beans et nous avons @EntityScan (de Spring Boot) pour rechercher les classes d'entités.

Nous avons également déclaré notre source de données simple - en utilisant une base de données intégrée qui exécutera le script SQL fourni au démarrage.

Il est maintenant temps de créer notre référentiel de données:

public interface StudentRepository extends JpaRepository {} 

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 – //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.

L'implémentation de ces exemples et tests se trouve dans le projet GitHub. Il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.

Pour exécuter le projet Spring boot, vous pouvez simplement faire mvn spring-boot: run et y accéder localement sur // localhost: 8080 /.

REST bas

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS