Часть 7: Описание демонстрационного теста#
Демонстрационный тест последовательно выполняет следующие проверки:
Создание объектов и получение их свойств.
Создание нескольких агрегатов и поиск по их свойствам.
Поиск по связанным сущностям.
Вывод всех книг.
Изменение свойств объекта.
Проверка идемпотентности вызовов.
Оптимистическая блокировка при обновлении объекта.
Для работы с тестами предназначен файл, расположенный по следующему пути: {root}/model-local-test/src/test/java/ru/test/ModelTest.java.
7.1. Создание объектов и получение их свойств#
Примечание
В рамках данного шага (функция
createBookStoreWithBook) будут созданы две связанные сущности: «книжный магазин» и «книга». В дополнение к этому свойства созданных сущностей будут выведены в лог-запись.
Все операции создания и изменения сущностей необходимо делать в рамках пакета команд. Операции создания связанных сущностей можно делать в рамках одного пакета:
Packet createPacket = new Packet();
Создайте тестовые сущности из нового пакета. В объектах пропишите требуемые связи. Для задания параметра необходимо использовать метод с префиксом set и названием свойства в camelCase (например, setAddress()):
// Создание магазина
BookStoreRef chitayGorodBookStoreRef = creationPacket.bookStore.create(bookstore -> {
bookstore.setAddress("Большая Садовая ул., 110 стр. 131, Ростов-на-Дону");
bookstore.setName("Читай-город");
});
// Создание книги
BookRef tomSawyerBookRef = creationPacket.book.create(book -> {
book.setAuthor("Марк Твен");
book.setName("Том Сойер и Гек Финн");
book.setIsbn("5-7516-0502-0");
book.setBookStore(chitayGorodBookStoreRef); // задание связи книги с магазином
});
Тесты создания и получения сущностей необходимо отправить на выполнение:
dataspaceCorePacketClient.execute(creationPacket);
Также можно создавать пакеты на чтение:
Packet readingPacket = new Packet();
Из созданных объектов DataSpace напрямую можно получить только идентификатор. Поэтому получать свойства объектов необходимо через класса-обертку с суффиксом Get. Для получения свойств объектов необходимо использовать методы с префиксом with:
BookGet tomSawyerBook = readingPacket.book.get(tomSawyerBookRef, book -> {
book.withAuthor();
book.withName();
book.withIsbn();
});
В демонстрационный тест добавлены проверки:
Assertions.assertEquals(tomSawyerBook.getAuthor(), "Марк Твен");
7.2. Создание объектов в нескольких агрегатах и поиск объектов по свойствам#
В рамках данного шага (функция createOtherBookStoreWithAnyBooks демонстрационного проекта) будет сделано следующее:
В существующий магазин будет добавлена новая книга.
Будет создан магазин.
Будут созданы книги.
Созданные книги будут добавлены в новый магазин.
В начале метода показан поиск объекта по значению параметра. Сначала необходимо сделать коллекцию с выборкой по требуемым свойствам объектов:
Для названия класса коллекции необходимо использовать суффикс
CollectionWith.Для создания коллекции необходимо использовать метод с префиксом
select(например,selectBookStoreиз классаGraphCreator).Для выбора получаемых свойств использовать методы с префиксом
with.Для задания условия выборки использовать метод
setWhereс лямбда-функцией типа*Grasp.Для оформления параметра использовать функцию с суффиксом
Eq(точное совпадение),Like(поиск по маске), илиContains(одно из значений).
BookStoreCollectionWith<BookStoreGrasp> bookStoreCollectionWith = GraphCreator.selectBookStore()
.setWhere(bookStoreGrasp -> bookStoreGrasp.nameLike("Читай-город%"));
GraphCollection<BookStoreGet> bookStoreGets = dataspaceCoreSearchClient.searchBookStore(bookStoreCollectionWith);
BookStoreRef bookStoreRef = BookStoreRef.of(bookStoreGets.get(0).getObjectId());
Между книжными магазинами и экземплярами книг действует соотношение «1:*». Поэтому операции создания объектов необходимо объединять в пакеты с разбивкой по объектам «Книжный магазин»:
// Создать пакет 1 — существующий магазин (BookStore 1)
Packet packet1 = new Packet();
// Создать книгу (Book 1) и добавить книгу в BookStore 1.
dataspaceCorePacketClient.execute(packet1);
// Создать пакет 2 — новый магазин магазин (BookStore 1)
Packet packet1 = new Packet();
// Создать книгу (Book 2) и добавить книгу в BookStore 2.
// Создать книгу (Book 3) и добавить книгу в BookStore 2.
dataspaceCorePacketClient.execute(packet2);
Для вывода информации по всем магазинам и всем книгам в магазине необходимо использовать Поиск DataSpace.
Предусмотрена сортировка коллекции объектов с помощью функции setSortingAdvanced. Для сортировки используются лямбды с суффиксом Picker (ниже пример сортировки по названию магазина в лексикографическом порядке по возрастанию).
//
BookStoreCollectionWith bookStoresCollectionWith = GraphCreator.selectBookStore()
.withName().withAddress()
.setSortingAdvanced(sort -> sort
.asc(fieldPicker -> fieldPicker.name())); // сортировка по названию магазина (по возрастанию)
// Запуск поиска
GraphCollection<BookStoreGet> bookStores = dataspaceCoreSearchClient.searchBookStore(bookStoresCollectionWith);
7.3. Поиск по связанным сущностям#
Тестовая функция createThirdBookStoreWithAnyBooks создает набор тестовых сущностей, в том числе экземпляров идентичных книг, распределенных по разным магазинам.
Особый интерес представляет поиск уникальных объектов. Для этого необходимо использовать метод с префиксом get.
BookStoreGet bookStoreBiblioGet =
dataspaceCoreSearchClient.getBookStore(spec -> spec.setWhere(bookStoreGrasp -> bookStoreGrasp.nameLike("Библио-Глобус%")));
Внимание!
Если по заданному условию будет найдено более одного элемента (например, если по запросу «Библио%» будут найдены магазины «Библио-Шар» и «Библиостар»), по запросу вернется исключение:
sbp.sbt.sdk.exception.detailedexception.TooManyResultsException: Найдено больше одной сущности.
7.4. Вывод всех объектов одного типа#
Тестовая функция searchAllBooksInAllBookStores создает коллекцию со свойствами всех книг, в том числе со свойствами всех связанных магазинов.
Особое внимание следует обратить на функцию setTotalCount, которая выводит общее число книг в связанном магазине.
BookCollectionWith bookCollectionWith = GraphCreator.selectBook()
.withName().withAuthor()
.withBookStore(bstWith -> bstWith
.withName().withAddress()
.withBooks(cw -> cw.setTotalCount(true)) // общее количество книг в магазине
)
.setSortingAdvanced(sort -> sort
.desc(fieldPicker -> fieldPicker.bookStore().booksCount())
.asc(BookGrasp::author).asc(BookGrasp::name)
.asc(fieldPicker -> fieldPicker.bookStore().name())
);
// Запустить поиск и получить как результат коллекцию
GraphCollection<BookGet> bookGets = dataspaceCoreSearchClient.searchBook(bookCollectionWith);
7.5. Изменение свойств объекта#
В начале функции searchByMaskAndUpdateAddressForBookStore производится поиск магазинов по адресу.
GraphCollection<BookStoreGet> bookStoresCollection =
dataspaceCoreSearchClient.searchBookStore(GraphCreator.selectBookStore()
.setWhere(bookStoreGrasp -> bookStoreGrasp.addressLike("Владимирский пр., 23, Санкт-Петербург%")));
Найденная коллекция магазинов обрабатывается в цикле: адрес каждого найденного магазина изменяется на новый. Для каждого магазина используется отдельный пакет. Для изменения свойств объекта необходимо использовать метод с суффиксом set.
for (int i = 0; i < bookStoresCollection.size(); ++i) {
BookStoreRef bookStoreRef = BookStoreRef.of(bookStoresCollection.get(i).getObjectId());
Packet updatingPacket = new Packet();
updatingPacket.bookStore.update(bookStoreRef, bookstore -> {
bookstore.setAddress("Невский пр., 28, Санкт-Петербург");
});
BookStoreGet bookStore = updatingPacket.bookStore.get(bookStoreRef, bookstore -> {
bookstore.withAddress();
bookstore.withName();
});
dataspaceCorePacketClient.execute(updatingPacket);
}
7.6. Проверка идемпотентности вызовов#
Идемпотентность — свойство вызовов, благодаря которому состояние агрегата не изменится при повторном идентичном вызове. Пример использования:
Микросервис создал книгу, но не успел вернуть результат.
Повторная команда создания этой книги не создаст дубликат книги, но вернет результат предыдущей исполненной команды.
В начале функции createBookWithIdempotence производится поиска магазина по названию.
Чтобы использовать идемпотентность для пакета, необходимо указать уникальный строчный ключ идемпотентности:
Packet creatingPacketWithIdempotence = Packet.builder().withIdempotencePacketId("CREATE STORE2 BookIdemp Непобедимый").build();
Внутри пакета создается объект:
BookRef bookRef = creatingPacketWithIdempotence.book.create(book -> {
book.setAuthor("Станислав Лем");
book.setName("Непобедимый");
book.setIsbn("978-5-17-060385-5");
book.setBookStore(biblioGlobusBookStoreRef);
});
После повторной попытки создания идентичного объекта из этого же пакета идентификатор объекта для первой переменной сравнивается с идентификатором второй переменной. Если идентификаторы совпали, дубликат объекта не был создан.
Assertions.assertEquals(bookRef.getId(), retryBookRef.getId());
7.7. Оптимистическая блокировка при обновлении объекта#
Оптимистическая блокировка не позволяет произвести изменения в агрегате, если между операциями чтения и записи он был изменен.
Рассмотрим тест подробнее.
Сначала выполняется поиск книги:
BookGet bookSearchByIsbn =
dataspaceCoreSearchClient.getBook(spec ->
spec.setWhere(bookGrasp ->
bookGrasp.isbnLike("978-5-17-060385-5%").
and(bookGrasp.bookStore().nameLike("Библио-Глобус%"))
)
);
BookRef bookRef = BookRef.of(bookSearchByIsbn.getObjectId());
После этого создается пакет с запросом версии агрегата (withAggregateVersionRequest), в котором вычитываются атрибуты и версия агрегата:
Packet readingPacketWithAggregateVersionRequest = Packet.builder().withAggregateVersionRequest().build();
BookGet bookGet = readingPacketWithAggregateVersionRequest.book.get(bookRef, book -> {
book.withName();
book.withAuthor();
book.withIsbn();
book.withBookStore(BookStoreWith::withName
);
});
После вычитки версии агрегат обновляется с повторной вычиткой версии агрегата:
Packet updatingPacket = Packet.builder().withAggregateVersion(3).build();
updatingPacket.book.update(BookRef.of(bookGet.getObjectId()), book -> {
book.setAuthor("Лем Станислав");
});
В конце метода фактическая версия агрегата проверяется на соответствие расчетной:
Assertions.assertEquals(4, readingPacketWithAggregateVersionRequestAfterUpdate.getAggregateVersion() );
Если между операцией вычитки агрегата с версией и обновлением агрегат будет изменен, то будет выдана ошибка следующего вида:
sbp.sbt.sdk.exception.detailedexception.AggregateVersionException:
Version 3 required but found 4 for object 6971773913230540804#ru.test.jpa.BookStore