«1. Обзор

В этой статье мы в основном сосредоточимся на реализации разбивки на страницы на стороне сервера в Spring REST API и простом внешнем интерфейсе AngularJS.

Мы также рассмотрим часто используемую сетку таблиц в Angular под названием UI Grid.

2. Зависимости

Здесь мы подробно описываем различные зависимости, необходимые для этой статьи.

2.1. JavaScript

Чтобы Angular UI Grid работал, нам понадобятся следующие скрипты, импортированные в наш HTML.

    Angular JS (1.5.8)
    Angular UI Grid

2.2. Maven

Для нашего бэкэнда мы будем использовать 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>

Примечание. Другие зависимости здесь не указаны, полный список см. в полном файле pom.xml в проект GitHub.

3. О приложении

Приложение представляет собой простое приложение-каталог учеников, которое позволяет пользователям видеть сведения об учениках в таблице с разбивкой на страницы.

Приложение использует Spring Boot и работает на встроенном сервере Tomcat со встроенной базой данных.

Наконец, что касается API, есть несколько способов разбиения на страницы, описанных в статье REST Pagination in Spring здесь, которую настоятельно рекомендуется прочитать вместе с этой статьей.

Наше решение здесь простое — иметь информацию о подкачке в запросе URI следующим образом: /student/get?page=1\u0026size=2.

4. Клиентская сторона

Во-первых, нам нужно создать логику клиентской стороны.

4.1. UI-Grid

Наш index.html будет иметь необходимые импорты и простую реализацию таблицы grid:

<!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>

Давайте посмотрим на код поближе:

    ng-app — это директива Angular, которая загружает приложение модуля. Все элементы под ними будут частью модуля приложения ng-controller — это директива Angular, которая загружает контроллер StudentCtrl с псевдонимом vm. Все элементы под ними будут частью ui-grid контроллера StudentCtrl — это директива Angular, которая принадлежит Angular ui-grid и использует gridOptions в качестве настроек по умолчанию, gridOptions объявлен в $scope в app.js

4.2 . Модуль AngularJS

Давайте сначала определим модуль в app.js:

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

Мы объявили модуль приложения и внедрили ui.grid, чтобы включить функциональность UI-Grid; мы также внедрили ui.grid.pagination, чтобы включить поддержку нумерации страниц.

Далее мы определим контроллер:

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

Давайте теперь посмотрим на пользовательские настройки пагинации в $scope.gridOptions:

    paginationPageSizes — определяет доступные параметры размера страницы paginationPageSize — “ определяет размер страницы по умолчанию enableColumnMenus — используется для включения/отключения меню в столбцах useExternalPagination — требуется, если вы разбиваете страницы на стороне сервера columnDefs — имена столбцов, которые будут автоматически сопоставлены с объектом JSON вернулся с сервера. Имена полей в объекте JSON, возвращенном с сервера, и определенное имя столбца должны совпадать. onRegisterApi — возможность регистрировать события публичных методов внутри сетки. Здесь мы зарегистрировали gridApi.pagination.on.paginationChanged, чтобы указать UI-Grid запускать эту функцию при каждом изменении страницы.

И отправить запрос к 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. Бэкенд и API

5.1. Служба RESTful

Вот простая реализация 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;
    }
}

@RestController был представлен в Spring 4.0 как удобная аннотация, которая неявно объявляет @Controller и @ResponseBody.

Для нашего API мы объявили, что он принимает два параметра: страницу и размер, которые также будут определять количество записей, возвращаемых клиенту.

Мы также добавили простую проверку, которая выдает исключение MyResourceNotFoundException, если номер страницы превышает общее количество страниц.

Наконец, мы вернем Page в качестве ответа — это очень полезный компонент Spring Data, который содержит данные разбиения на страницы.

5.2. Реализация службы

Наша служба просто возвращает записи на основе страницы и размера, предоставленных контроллером:

@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. Реализация репозитория

«Для нашего уровня сохраняемости мы используем встроенную базу данных и Spring Data JPA.

Во-первых, нам нужно настроить нашу конфигурацию сохраняемости:

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

Конфигурация сохраняемости проста — у нас есть @EnableJpaRepositories для сканирования указанного пакета и поиска интерфейсов репозитория Spring Data JPA.

Здесь у нас есть @ComponentScan для автоматического сканирования всех bean-компонентов, и у нас есть @EntityScan (из Spring Boot) для сканирования классов сущностей.

Мы также объявили наш простой источник данных — используя встроенную базу данных, которая будет запускать сценарий SQL, предоставленный при запуске.

Теперь пришло время создать наш репозиторий данных:

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

Это в основном все, что нам нужно сделать здесь; если вы хотите глубже узнать, как настроить и использовать очень мощный Spring Data JPA, обязательно прочитайте руководство по нему здесь.

6. Запрос и ответ на разбиение на страницы

При вызове API – http://localhost:8080/student/get?page=1\u0026size=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
}

~ ~~ Здесь следует отметить, что сервер возвращает DTO org.springframework.data.domain.Page, обертывая наши ресурсы для учащихся.

Объект Page будет иметь следующие поля:

    last — установите значение true, если это последняя страница, иначе false first — установите значение true, если это первая страница, иначе false totalElements — общее количество строк/записей. В нашем примере мы передали это параметру сетки пользовательского интерфейса $scope.gridOptions.totalItems, чтобы определить, сколько страниц будет доступно. количество записей на странице, это было передано от клиента через параметр size number — номер страницы, отправленный клиентом, в нашем ответе число равно 0, потому что в нашем бэкэнде мы используем массив студентов, который является нулевым. на основе индекса, поэтому в нашем бэкенде мы уменьшаем номер страницы на 1 sort — параметр сортировки для страницы numberOfElements — количество строк/записей, возвращаемых для страницы

7. Тестирование нумерации страниц

Давайте теперь настройте тест для нашей логики разбиения на страницы, используя RestAssured; чтобы узнать больше о RestAssured, вы можете ознакомиться с этим руководством.

7.1. Подготовка теста

Для простоты разработки нашего тестового класса мы добавим статический импорт:

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

Далее мы настроим тест с поддержкой Spring:

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

@SpringApplicationConfiguration помогает Spring знает, как загрузить ApplicationContext, в этом случае мы использовали Application.java для настройки нашего ApplicationContext.

@WebAppConfiguration был определен, чтобы сообщить Spring, что загружаемый ApplicationContext должен быть WebApplicationContext.

И @IntegrationTest был определен для запуска запуска приложения при запуске теста, что делает наши службы REST доступными для тестирования.

7.2. Тесты

Вот наш первый тестовый пример:

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

Этот тестовый пример выше предназначен для проверки того, что когда страница 1 и размер 2 передаются службе REST, содержимое JSON, возвращаемое с сервера, должно иметь имена Bryan и Бен.

Давайте разберем тестовый пример:

    задано — часть RestAssured и используется для начала построения запроса, вы также можете использовать with() get — часть RestAssured и, если используется, запускает получение request, используйте post() для пост-запроса hasItems — часть hamcrest, которая проверяет совпадение значений

Мы добавляем еще несколько тестов: фактически вызывается, получен ответ OK:

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

Этот тест утверждает, что когда запрашивается размер страницы, равный двум, размер возвращаемой страницы фактически равен двум:

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

Этот тест утверждает, что при вызове ресурсов в первый раз, когда значение имени первой страницы истинно.

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

В репозитории есть еще много тестов, поэтому обязательно загляните в проект GitHub.

8. Заключение

В этой статье показано, как реализовать сетку таблицы данных с помощью UI-Grid в AngularJS и как реализовать необходимое разбиение на страницы на стороне сервера.

«Реализацию этих примеров и тестов можно найти в проекте GitHub. Это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.

Чтобы запустить загрузочный проект Spring, вы можете просто выполнить mvn spring-boot:run и получить к нему локальный доступ по адресу http://localhost:8080/.

«