Paginação com Spring REST e tabela AngularJS

Paginação com Spring REST e tabela AngularJS

1. Visão geral

Neste artigo, vamos nos concentrar principalmente na implementação da paginação do lado do servidor em umSpring REST APIe um front-end AngularJS simples.

Também exploraremos uma grade de tabela comumente usada em Angular chamadaUI Grid.

2. Dependências

Aqui, detalhamos várias dependências necessárias para este artigo.

2.1. Javascript

Para que o Angular UI Grid funcione, precisaremos dos scripts abaixo importados em nosso HTML.

2.2. Maven

Para nosso back-end, usaremosSpring Boot, portanto, precisaremos das dependências abaixo:


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


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

Note: Outras dependências não foram especificadas aqui, para a lista completa, verifique opom.xml completo no projetoGitHub.

3. Sobre o aplicativo

O aplicativo é um aplicativo simples de diretório do aluno que permite aos usuários ver os detalhes do aluno em uma grade de tabela paginada.

O aplicativo usaSpring Boote é executado em um servidor Tomcat integrado com um banco de dados integrado.

Finalmente, no lado da API, existem algumas maneiras de fazer a paginação, descritas emREST Pagination in Spring article here - que é altamente recomendável ler em conjunto com este artigo.

Nossa solução aqui é simples - ter as informações de paginação em uma consulta URI da seguinte forma:/student/get?page=1&size=2.

4. O lado do cliente

Primeiro, precisamos criar a lógica do lado do cliente.

4.1. O UI-Grid

Nossoindex.html terá as importações de que precisamos e uma implementação simples da grade da tabela:



    
        
        
        
        
    
    
        

Vamos dar uma olhada no código:

  • ng-app - é a diretiva Angular que carrega o móduloapp. Todos os elementos sob estes farão parte do móduloapp

  • ng-controller - é a diretiva angular que carrega o controladorStudentCtrl com um alias devm. Todos os elementos sob estes farão parte do controladorStudentCtrl

  • ui-grid - é a diretiva Angular que pertence ao Angularui-grid e usagridOptions como suas configurações padrão,gridOptions é declarado em$scope emapp.js

4.2. O Módulo AngularJS

Vamos primeiro definir o módulo emapp.js:

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

Declaramos o móduloapp e injetamosui.grid para habilitar a funcionalidade UI-Grid; também injetamosui.grid.pagination para habilitar o suporte à paginação.

A seguir, definiremos o controlador:

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

Vamos agora dar uma olhada nas configurações de paginação personalizadas em$scope.gridOptions:

  • paginationPageSizes - define as opções de tamanho de página disponíveis

  • paginationPageSize - define o tamanho da página padrão

  • enableColumnMenus - é usado para habilitar / desabilitar o menu nas colunas

  • useExternalPagination - é necessário se você estiver paginando no lado do servidor

  • columnDefs - os nomes das colunas que serão mapeados automaticamente para o objeto JSON retornado do servidor. Os nomes dos campos no objeto JSON retornados do servidor e o nome da coluna definido devem corresponder.

  • onRegisterApi - a capacidade de registrar eventos de métodos públicos dentro da grade. Aqui, registramos ogridApi.pagination.on.paginationChanged para dizer ao UI-Grid para acionar esta função sempre que a página for alterada.

E para enviar a solicitação para a 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. O back-end e a API

5.1. O serviço RESTful

Esta é a implementação simples da API RESTful com suporte de paginação:

@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;
    }
}

O@RestController foi introduzido no Spring 4.0 como uma anotação de conveniência que declara implicitamente@Controllere@ResponseBody.

Para a nossa API, declaramos que ela aceita dois parâmetros que sãopagee size que também determinariam o número de registros a serem retornados ao cliente.

Também adicionamos uma validação simples que lançará umMyResourceNotFoundException se o número da página for maior que o total de páginas.

Finalmente, retornaremosPage como a Resposta - este é um componente muito útil de Spring Data que contém dados de paginação.

5.2. A implementação do serviço

Nosso serviço simplesmente retornará os registros com base na página e no tamanho fornecidos pelo controlador:

@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. A Implementação do Repositório

Para nossa camada de persistência, estamos usando um banco de dados embutido e Spring Data JPA.

Primeiro, precisamos definir nossa configuração de persistência:

@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;
    }
}

A configuração de persistência é simples - temos@EnableJpaRepositories para escanear o pacote especificado e encontrar nossas interfaces de repositório Spring Data JPA.

Temos@ComponentScan aqui para verificar automaticamente todos os beans ehave @EntityScan (do Spring Boot) para verificar as classes de entidade.

Também declaramos nossa fonte de dados simples - usando um banco de dados incorporado que executará o script SQL fornecido na inicialização.

Agora é hora de criar nosso repositório de dados:

public interface StudentRepository extends JpaRepository {}

Isso é basicamente tudo que precisamos fazer aqui; se você deseja se aprofundar em como configurar e usar o altamente poderoso Spring Data JPA, definitivamenteread the guide to it here.

6. Solicitação e resposta de paginação

Ao chamar a APIhttp://localhost:8080/student/get?page=1&size=5, a resposta JSON será semelhante a esta:

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

Uma coisa a notar aqui é que o servidor retorna um DTOorg.springframework.data.domain.Page, envolvendo nossos RecursosStudent.

O objetoPage terá os seguintes campos:

  • last - definido comotrue se for a última página, caso contrário, falso

  • first - defina comotrue se for a primeira página, caso contrário, é falso

  • totalElements - o número total de linhas / registros. Em nosso exemplo, passamos isso para as opçõesui-grid$scope.gridOptions.totalItems para determinar quantas páginas estarão disponíveis

  • totalPages - o número total de páginas derivadas de (totalElements / size)

  • size - o número de registros por página, foi passado do cliente por meio do parâmetrosize

  • number - o número da página enviado pelo cliente, em nossa resposta o número é 0 porque em nosso backend estamos usando um array deStudents que é um índice baseado em zero, então em nosso backend, nós diminuir o número da página em 1

  • sort - o parâmetro de classificação para a página

  • numberOfElements - o número de linhas / registros retornados para a página

7. Testando Paginação

Vamos agora configurar um teste para nossa lógica de paginação, usandoRestAssured; para aprender mais sobreRestAssured, você pode dar uma olhada nestetutorial.

7.1. Preparando o Teste

Para facilitar o desenvolvimento de nossa classe de teste, adicionaremos as importações estáticas:

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

A seguir, vamos configurar o teste habilitado para Spring:

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

O@SpringApplicationConfiguration ajuda o Spring a saber como carregar oApplicationContext, neste caso, usamos oApplication.java para configurar nossoApplicationContext.

O@WebAppConfiguration foi definido para dizer ao Spring que oApplicationContext a ser carregado deve ser umWebApplicationContext.

E o@IntegrationTest foi definido para acionar a inicialização do aplicativo ao executar o teste, isso torna nossos serviços REST disponíveis para teste.

7.2. Os testes

Aqui está o nosso primeiro caso de teste:

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

Este caso de teste acima é para testar se quando a página 1 e o tamanho 2 são passados ​​para o serviço REST, o conteúdo JSON retornado do servidor deve ter os nomesBryaneBen.

Vamos dissecar o caso de teste:

  • given - a parte deRestAssured e é usado para começar a construir a solicitação, você também pode usarwith()

  • get - a parte deRestAssured e se usado acionar uma solicitação get, use post () para a solicitação post

  • hasItems - a parte do hamcrest que verifica se os valores têm alguma correspondência

Adicionamos mais alguns casos de teste:

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

Este teste afirma que, quando o ponto é realmente chamado, uma resposta OK é recebida:

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

Este teste afirma que quando o tamanho da página de dois é solicitado, o tamanho da página retornado é na verdade dois:

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

Este teste afirma que, quando os recursos são chamados pela primeira vez, o valor do nome da primeira página é verdadeiro.

Existem muitos mais testes no repositório, então definitivamente dê uma olhada no projetoGitHub.

8. Conclusão

Este artigo ilustrou como implementar uma grade de tabela de dados usandoUI-Grid emAngularJS e como implementar a paginação do lado do servidor necessária.

A implementação desses exemplos e testes pode ser encontrada emGitHub project. Como é um projeto do Maven, deve ser fácil importar e executar como está.

Para executar o projeto de inicialização Spring, você pode simplesmente fazermvn spring-boot:rune acessá-lo localmente emhttp://localhost:8080/.