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 API– http://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/.