Pagination avec Spring REST et table AngularJS

Pagination avec table Spring REST et AngularJS

1. Vue d'ensemble

Dans cet article, nous nous concentrerons principalement sur l'implémentation de la pagination côté serveur dans unSpring REST API et un simple front-end AngularJS.

Nous allons également explorer une grille de table couramment utilisée dans Angular nomméeUI Grid.

2. Les dépendances

Nous détaillons ici diverses dépendances requises pour cet article.

2.1. JavaScript

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

2.2. Maven

Pour notre backend, nous utiliseronsSpring 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

Note: D'autres dépendances n'ont pas été spécifiées ici, pour la liste complète, vérifiez lespom.xml complets dans le projetGitHub.

3. À propos de l'application

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

L'application utiliseSpring Boot et s'exécute sur 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 leREST Pagination in Spring article here - qui est fortement recommandée à lire en conjonction 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. Le côté client

Premièrement, nous devons créer la logique côté client.

4.1. Le UI-Grid

Nosindex.html auront les importations dont nous avons besoin et une implémentation simple de la grille de table:



    
        
        
        
        
    
    
        

Examinons de plus près le code:

  • ng-app - est la directive angulaire qui charge le moduleapp. Tous les éléments sous ceux-ci feront partie du moduleapp

  • ng-controller - est la directive angulaire qui charge le contrôleurStudentCtrl avec un alias devm. Tous les éléments sous ceux-ci feront partie du contrôleurStudentCtrl

  • ui-grid - est la directive angulaire qui appartient à Angularui-grid et utilisegridOptions comme paramètres par défaut,gridOptions est déclaré sous$scope dansapp.js

4.2. Le module AngularJS

Définissons d'abord le module enapp.js:

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

Nous avons déclaré le moduleapp et nous avons injectéui.grid pour activer la fonctionnalité UI-Grid; nous avons également injectéui.grid.pagination pour activer le support 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;
                 });
            });
        }
    };
}]);

Examinons maintenant les 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é lesgridApi.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 demande à 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 sontpage et taille qui détermineraient également le nombre d'enregistrements à renvoyer au client.

Nous avons également ajouté une simple validation qui lancera unMyResourceNotFoundException si le numéro de page est supérieur au nombre total de pages.

Enfin, nous retourneronsPage en tant que réponse - c'est un composant très utile de Spring 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.

Premièrement, nous devons configurer notre configuration de persistance:

@EnableJpaRepositories("org.example.web.dao")
@ComponentScan(basePackages = { "org.example.web" })
@EntityScan("org.example.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 les@ComponentScan ici pour rechercher automatiquement tous les beans et noushave @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 {}

C'est essentiellement tout ce que nous devons faire ici; si vous souhaitez approfondir la configuration et l'utilisation du très puissant Spring Data JPA, certainementread the guide to it here.

6. Demande de pagination et réponse

Lors de l'appel de l'APIhttp://localhost:8080/student/get?page=1&size=5, la réponse JSON ressemblera à ceci:

{
    "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
}

Une chose à noter ici est que le serveur renvoie un DTOorg.springframework.data.domain.Page, enveloppant nos ressourcesStudent.

L'objetPage aura les champs suivants:

  • last - mis àtrue si c'est la dernière page sinon faux

  • first - défini surtrue s'il s'agit de la première page sinon faux

  • totalElements - le nombre total de lignes / enregistrements. Dans notre exemple, nous avons transmis ceci aux optionsui-grid$scope.gridOptions.totalItems pour déterminer combien de pages seront disponibles

  • totalPages - le nombre total de pages dérivé de (totalElements / size)

  • size - le nombre d'enregistrements par page, il a été transmis par le client via le paramètresize

  • number - le numéro de page envoyé par le client, dans notre réponse le nombre est 0 car dans notre backend nous utilisons un tableau deStudents qui est un index de base zéro, donc dans notre backend, nous décrémente le numéro de page de 1

  • sort - le paramètre de tri pour la page

  • numberOfElements - le nombre de lignes / enregistrements retournés pour la page

7. Test de la pagination

Configurons maintenant un test pour notre logique de pagination, en utilisantRestAssured; pour en savoir plus surRestAssured, vous pouvez jeter un œil à cetutorial.

7.1. Préparation du test

Pour faciliter le développement de notre classe de test, nous ajouterons les importations statiques:

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

Ensuite, nous allons configurer le test Spring activé:

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

Le@SpringApplicationConfiguration aide Spring à savoir comment charger lesApplicationContext, dans ce cas, nous avons utilisé lesApplication.java pour configurer nosApplicationContext.

Le@WebAppConfiguration a été défini pour indiquer à Spring que leApplicationContext à charger doit être unWebApplicationContext.

Et le@IntegrationTest a été défini pour déclencher le démarrage de l'application lors de l'exécution du test, cela rend nos services REST disponibles pour les tests.

7.2. Les tests

Voici notre premier cas de test:

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

Ce cas de test ci-dessus consiste à tester que lorsque la page 1 et la taille 2 sont transmises au service REST, le contenu JSON renvoyé par le serveur doit avoir les nomsBryan etBen.

Décortiquons le cas de test:

  • given - la partie deRestAssured et est utilisée pour commencer à construire la demande, vous pouvez également utiliserwith()

  • get - la partie deRestAssured et si elle est utilisée, elle déclenche une demande d'obtention, utilise post () pour la demande de publication

  • hasItems - la partie de hamcrest qui vérifie si les valeurs correspondent

Nous ajoutons quelques cas de test supplémentaires:

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

Ce test confirme que lorsque le point est appelé, une réponse OK est reçue:

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

Ce test affirme que lorsque la taille de page de deux est demandée, la taille de page renvoyée est en réalité de deux:

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

Ce test confirme que lorsque les ressources sont appelées pour la première fois, la valeur du nom de la première page est true.

Il y a beaucoup plus de tests dans le référentiel, alors jetez un œil au projetGitHub.

8. Conclusion

Cet article a illustré comment implémenter une grille de table de données à l'aide deUI-Grid dansAngularJS et comment implémenter la pagination côté serveur requise.

L'implémentation de ces exemples et tests peut être trouvée dans lesGitHub project. Ceci est un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.

Pour exécuter le projet de démarrage Spring, vous pouvez simplement fairemvn spring-boot:run et y accéder localement surhttp://localhost:8080/.