Pagination with Spring REST and AngularJS table – 用Spring REST和AngularJS表格进行分页

最后修改: 2016年 8月 17日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this article, we will mainly focus on implementing server side pagination in a Spring REST API and a simple AngularJS front-end.

在这篇文章中,我们将主要关注在Spring REST API和一个简单的AngularJS前端实现服务器端分页。

We’ll also explore a commonly used table grid in Angular named UI Grid.

我们还将探讨Angular中一个常用的表格网格,名为UI网格

2. Dependencies

2.依赖性

Here we detail various dependencies that are required for this article.

这里我们详细介绍一下本文所需的各种依赖关系。

2.1. JavaScript

2.1.Javascript

In order for Angular UI Grid to work, we will need the below scripts imported in our HTML.

为了让Angular UI网格工作,我们需要在我们的HTML中导入以下脚本。

2.2. Maven

2.2.Maven

For our backend we will be using Spring Boot, so we’ll need the below dependencies:

对于我们的后端,我们将使用Spring Boot,所以我们将需要以下依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>

Note: Other dependencies were not specified here, for the full list, check the complete pom.xml in the GitHub project.

注意:这里没有指定其他的依赖关系,关于完整的列表,请查看GitHub项目中完整的pom.xml

3. About the Application

3.关于申请

The application is a simple student’s directory app which allows users to see the student details in a paginated table grid.

该应用程序是一个简单的学生目录应用程序,允许用户在一个分页的表格网格中看到学生的详细信息。

The application uses Spring Boot and runs in an embedded Tomcat server with an embedded database.

该应用程序使用Spring Boot,并在一个具有嵌入式数据库的嵌入式Tomcat服务器中运行。

Finally, on the API side of things, there are a few ways to do pagination, described in the REST Pagination in Spring article here – which is highly recommended reading in conjunction with this article.

最后,在API方面,有几种方法可以进行分页,在REST Pagination in Spring文章中描述了这一点–强烈建议与本文一起阅读。

Our solution here is simple – having the paging information in a URI query as follows: /student/get?page=1&size=2.

我们的解决方案很简单–在URI查询中拥有分页信息,如下所示。/student/get?page=1&size=2

4. The Client Side

4.客户端

First, we need to create the client-side logic.

首先,我们需要创建客户端的逻辑。

4.1. The UI-Grid

4.1.UI-Grid

Our index.html will have the imports we need and a simple implementation of the table grid:

我们的index.html将有我们需要的导入和一个简单的表格网格实现。

<!DOCTYPE html>
<html lang="en" ng-app="app">
    <head>
        <link rel="stylesheet" href="https://cdn.rawgit.com/angular-ui/
          bower-ui-grid/master/ui-grid.min.css">
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/
          1.5.6/angular.min.js"></script>
        <script src="https://cdn.rawgit.com/angular-ui/bower-ui-grid/
          master/ui-grid.min.js"></script>
        <script src="view/app.js"></script>
    </head>
    <body>
        <div ng-controller="StudentCtrl as vm">
            <div ui-grid="gridOptions" class="grid" ui-grid-pagination>
            </div>
        </div>
    </body>
</html>

Let’s have a closer look at the code:

让我们仔细看看这段代码。

  • ng-app – is the Angular directive that loads the module app. All elements under these will be part of the app module
  • ng-controller – is the Angular directive that loads the controller StudentCtrl with an alias of vm. All elements under these will be part of the StudentCtrl controller
  • ui-grid – is the Angular directive that belongs to Angular ui-grid and uses gridOptions as its default settings, gridOptions is declared under $scope in app.js

4.2. The AngularJS Module

4.2.AngularJS模块

Let’s first define the module in app.js:

让我们首先在app.js中定义该模块。

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

We declared the app module and we injected ui.grid to enable UI-Grid functionality; we also injected ui.grid.pagination to enable pagination support.

我们声明了app模块,我们注入了ui.grid以启用UI-Grid功能;我们还注入了ui.grid.pagination以启用分页支持。

Next, we’ll define the controller:

接下来,我们将定义控制器。

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

Let’s now have a look at the custom pagination settings in $scope.gridOptions:

现在让我们看一下$scope.gridOptions中的自定义分页设置。

  • paginationPageSizes – defines the available page size options
  • paginationPageSize – defines the default page size
  • enableColumnMenus – is used to enable/disable the menu on columns
  • useExternalPagination – is required if you are paginating on the server side
  • columnDefs – the column names that will be automatically mapped to the JSON object returned from the server. The field names in the JSON Object returned from the server and the column name defined should match.
  • onRegisterApi – the ability to register public methods events inside the grid. Here we registered the gridApi.pagination.on.paginationChanged to tell UI-Grid to trigger this function whenever the page was changed.

And to send the request to the API:

并向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. The Backend and the API

5.后台和API

5.1. The RESTful Service

5.1.RESTful服务

Here’s the simple RESTful API implementation with pagination support:

这里是简单的RESTful API实现,支持分页。

@RestController
public class StudentDirectoryRestController {

    @Autowired
    private StudentService service;

    @RequestMapping(
      value = "/student/get", 
      params = { "page", "size" }, 
      method = RequestMethod.GET
    )
    public Page<Student> findPaginated(
      @RequestParam("page") int page, @RequestParam("size") int size) {

        Page<Student> resultPage = service.findPaginated(page, size);
        if (page > resultPage.getTotalPages()) {
            throw new MyResourceNotFoundException();
        }

        return resultPage;
    }
}

The @RestController was introduced in Spring 4.0 as a convenience annotation which implicitly declares @Controller and @ResponseBody.

@RestController在Spring 4.0中作为一个方便的注解被引入,它隐含地声明了@Controller@ResponseBody。

For our API, we declared it to accept two parameters which are page and size that would also determine the number of records to return to the client.

对于我们的API,我们声明它接受两个参数,即page和size,这也将决定返回给客户端的记录数量。

We also added a simple validation that will throw a MyResourceNotFoundException if the page number is higher than the total pages.

我们还添加了一个简单的验证,如果页数高于总页数,将抛出一个MyResourceNotFoundException

Finally, we’ll return Page as the Response – this is a super helpful component of Spring Data which has held pagination data.

最后,我们将返回Page作为响应–这是Spring Data的一个超级有用的组件,它已经持有分页数据。

5.2. The Service Implementation

5.2.服务的实施

Our service will simply return the records based on page and size provided by the controller:

我们的服务将简单地根据控制器提供的页面和大小来返回记录。

@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentRepository dao;

    @Override
    public Page<Student> findPaginated(int page, int size) {
        return dao.findAll(new PageRequest(page, size));
    }
}

5.3. The Repository Implementation

5.3.存储库的实现

For our persistence layer, we’re using an embedded database and Spring Data JPA.

对于我们的持久层,我们正在使用一个嵌入式数据库和Spring Data JPA。

First, we need to setup our persistence config:

首先,我们需要设置我们的持久性配置。

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

The persistence config is simple – we have @EnableJpaRepositories to scan the specified package and find our Spring Data JPA repository interfaces.

持久性配置很简单–我们有@EnableJpaRepositories来扫描指定的包并找到我们的Spring Data JPA仓库接口。

We have the @ComponentScan here to automatically scan for all beans and we have @EntityScan (from Spring Boot) to scan for entity classes.

我们有@ComponentScan 来自动扫描所有Bean,我们有@EntityScan (来自Spring Boot)来扫描实体类。

We also declared our simple datasource – using an embedded database that will run the SQL script provided on startup.

我们还声明了我们的简单数据源–使用一个嵌入式数据库,它将在启动时运行提供的SQL脚本。

Now it’s time we create our data repository:

现在是我们创建数据存储库的时候了。

public interface StudentRepository extends JpaRepository<Student, Long> {}

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.

这基本上就是我们在这里需要做的所有事情;如果您想深入了解如何设置和使用功能强大的 Spring Data JPA,请务必阅读这里的指南

6. Pagination Request and Response

6.分页请求和响应

When calling the API – http://localhost:8080/student/get?page=1&size=5, the JSON response will look something like this:

当调用API时– http://localhost:8080/student/get?page=1&size=5,JSON响应将看起来像这样。

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

这里需要注意的是,服务器返回一个org.springframework.data.domain.Page DTO,包装了我们的Student资源。

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

7.测试分页

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.

现在让我们为我们的分页逻辑设置一个测试,使用RestAssured;要了解更多关于RestAssured的信息,你可以看一下这个tutorial

7.1. Preparing the Test

7.1.准备测试

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:

接下来,我们将设置Spring启用的测试。

@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.

@SpringApplicationConfiguration帮助Spring知道如何加载ApplicationContext,在本例中,我们使用Application.java来配置我们的ApplicationContext。

The @WebAppConfiguration was defined to tell Spring that the ApplicationContext to be loaded should be a WebApplicationContext.

@WebAppConfiguration被定义为告诉Spring要加载的ApplicationContext应该是一个WebApplicationContext。

And the @IntegrationTest was defined to trigger the application startup when running the test, this makes our REST services available for testing.

@IntegrationTest被定义为运行测试时触发应用程序的启动,这使得我们的REST服务可用于测试。

7.2. The Tests

7.2.测试

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.

上面这个测试案例是为了测试当第1页和第2页被传递给REST服务时,从服务器返回的JSON内容应该有BryanBen.的名字。

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:

该测试断言,当该点被实际调用时,会收到OK响应。

@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:

这个测试断言,当要求页面大小为2时,返回的页面大小实际上是2。

@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.

储存库中还有许多测试,所以一定要看看GitHub项目。

8. Conclusion

8.结论

This article illustrated how to implement a data table grid using UI-Grid in AngularJS and how to implement the required server-side pagination.

本文说明了如何使用UI-GridAngularJS中实现一个数据表网格,以及如何实现所需的服务器端分页。

The implementation of these examples and tests can be found in the GitHub project. This is a Maven project, so it should be easy to import and run as it is.

这些例子和测试的实现可以在GitHub项目中找到。这是一个Maven项目,所以应该很容易导入并按原样运行。

To run the Spring boot project, you can simply do mvn spring-boot:run and access it locally on http://localhost:8080/.

要运行Spring boot项目,你可以简单地执行mvn spring-boot:run,并在本地http://localhost:8080/。访问它。