1. Обзор
В этой статье мы в основном сосредоточимся на реализации разбивки на страницы на стороне сервера в Spring REST API
и простом внешнем интерфейсе AngularJS.
Мы также рассмотрим часто используемую сетку таблиц в Angular под названием UI Grid .
2. Зависимости
Здесь мы подробно описываем различные зависимости, необходимые для этой статьи.
2.1. JavaScript
Чтобы Angular UI Grid работал, нам понадобятся следующие скрипты, импортированные в наш HTML.
Угловой JS (1.5.8)
Угловая сетка пользовательского интерфейса
2.2. Мавен
Для нашего бэкенда мы будем использовать 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&size=2
.
4. Клиентская сторона
Во-первых, нам нужно создать логику на стороне клиента.
4.1. UI-сетка
Наш 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>
Давайте внимательно посмотрим на код:
ng-app
— это директива Angular, которая загружаетприложение
модуля . Все элементы под ними будут частью модуляприложения .
ng-controller
— это директива Angular, которая загружает контроллерStudentCtrl
с псевдонимомvm.
Все элементы под ними будут частью контроллераStudentCtrl.
ui-grid
— это директива Angular, которая принадлежит Angularui-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.foreach.web.dao")
@ComponentScan(basePackages = { "com.foreach.web" })
@EntityScan("com.foreach.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&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
}
Здесь следует отметить, что сервер возвращает DTO org.springframework.data.domain.Page
, обертывая наши студенческие
ресурсы.
Объект Page
будет иметь следующие поля:
last
— установите значениеtrue
, если это последняя страница, в противном случае falsefirst
– установите значениеtrue
, если это первая страница, иначе falsetotalElements
— общее количество строк/записей. В нашем примере мы передали это параметрусетки
пользовательского интерфейса $scope.gridOptions.totalItems
, чтобы определить, сколько страниц будет доступно .totalPages
— общее количество страниц, которые были получены из (totalElements / size
)size
— количество записей на странице, это было передано от клиента черезразмер параметра
число
— номер страницы, отправленный клиентом, в нашем ответе число равно 0, потому что в нашем бэкэнде мы используем массивStudent
s, который является индексом, начинающимся с нуля, поэтому в нашем бэкэнде мы уменьшаем номер страницы на 1sort
— параметр сортировки страницы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, возвращаемое с сервера, должно иметь имена Брайан
и Бен.
Разберем тестовый пример:
данный
— частьRestAssured
и используется для начала построения запроса, вы также можете использоватьwith()
get
— частьRestAssured
, и если она используется, инициируется запрос на получение, используйте post() для почтового запроса.hasItems
— часть hamcrest, которая проверяет совпадение значений.
Добавим еще несколько тестовых случаев:
@Test
public void givenRequestForStudents_whenResourcesAreRetrievedPaged_thenExpect200() {
given().params("page", "0", "size", "2").get(ENDPOINT)
.then()
.statusCode(200);
}
Этот тест утверждает, что при фактическом вызове точки получен ответ OK:
@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/.