Часть 7: Описание демонстрационного теста#

Демонстрационный тест последовательно выполняет следующие проверки:

  1. Создание объектов и получение их свойств.

  2. Создание нескольких агрегатов и поиск по их свойствам.

  3. Поиск по связанным сущностям.

  4. Вывод всех книг.

  5. Изменение свойств объекта.

  6. Проверка идемпотентности вызовов.

  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 демонстрационного проекта) будет сделано следующее:

  1. В существующий магазин будет добавлена новая книга.

  2. Будет создан магазин.

  3. Будут созданы книги.

  4. Созданные книги будут добавлены в новый магазин.

В начале метода показан поиск объекта по значению параметра. Сначала необходимо сделать коллекцию с выборкой по требуемым свойствам объектов:

  • Для названия класса коллекции необходимо использовать суффикс 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. Проверка идемпотентности вызовов#

Идемпотентность — свойство вызовов, благодаря которому состояние агрегата не изменится при повторном идентичном вызове. Пример использования:

  1. Микросервис создал книгу, но не успел вернуть результат.

  2. Повторная команда создания этой книги не создаст дубликат книги, но вернет результат предыдущей исполненной команды.

В начале функции 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