Описание работы посредством DataSpace Core SDK#
Последовательность выполнения#
Изучение общей информации об указанном SDK.
Для языков программирования Java и Kotlin DataSpace Core (DSPC) предоставляет SDK, позволяющий использовать типизированные конструкции на стороне клиентского приложения. В данном документе приводится описание работы посредством данного SDK.
Также возможно взаимодействие с серверной частью DataSpace посредством:
протокола JSON-RPC 2.0 — данное описание приводится в документе «Протокол JSON-RPC 2.0 в применении к контроллерам модуля»;
протокола GraphQL — данное описание приводится в документе «Протокол GraphQL».
Подробные сведения о способах реализации модели предметной области можно найти в документе «Руководство по ведению модели данных».
Выполнение необходимых для решения конкретной задачи действий. Описание различных задач и сценариев приведено ниже.
Генерация рабочего артефакта#
Рабочий артефакт должен включать в себя следующие элементы:
скомпилированные Java-классы;
архивы со скомпилированными Java-классами в формате JAR.
Сборку рабочего артефакта необходимо производить с помощью Maven. Правила формирования и структура создаваемых артефактов подробно описаны в документе «Структура артефактов DataSpace».
Локальный запуск приложения#
Локальный запуск подробно описан в документе «Быстрый старт».
Использование SDK для формирования пакета команд#
SDK, предоставляемый DataSpace, взаимодействует с серверной частью по протоколу JSON-RPC 2.0 через точку доступа со следующим URL-адресом: {серверURL}/packet.
Для работы с пакетом изменений в SDK представлены следующие классы:
DataspaceCorePacketClient: клиент для взаимодействия с контроллером сервиса по обслуживанию пакета измененийPacket: генерируемый на основе пользовательской модели адаптер протокола для работы с пакетом изменений
В примерах используется модель.
Клиент DataspaceCorePacketClient#
Функция клиента заключается в передаче пакета или группы пакетов сервису, получения и разбора ответа. При создании клиента необходимо указать адрес сервиса:
DataspaceCorePacketClient packetClient = new DataspaceCorePacketClient("http://127.0.0.1:8080");
Подробно о вариантах клиента и возможностях конфигурации описано в разделе «DataSpace-клиенты в DataSpace Java SDK».
Вызов сервиса для выполнения пакета производится методом execute:
packetClient.execute(Packet.createPacket());
Параметр метода принимает пакет изменений, который должен исполнить сервис. Класс Packet рассмотрен в разделе «Пакет изменений Packet».
Если выполнение метода не вызвало исключения, то пакет обработан успешно. В противном случае будет возбуждено
SdkJsonRpcClientException или наследованное от этого класса исключение:
ObjectNotFoundException: сущность с указанным идентификатором не найдена;ParseException: ошибка разбора запроса;InvalidArgumentException: ошибка аргумента в запросе;DataAccessException: ошибка уровня базы данных;DataAccessConstraintException: ошибка уровня базы данных о нарушении ограничения;IdempotencyException: ошибка идемпотентного вызова;StatusException: ошибка статуса;AggregateException: ошибка использования агрегата;AggregateVersionException: ошибка версии агрегата;SystemLockException: ошибка системной блокировки;ApplicationLockException: ошибка прикладной блокировки;MaskNotMatchException: ошибка проверки значения маске;CompareNotEqualException: ошибка сравнения значений;ConnectTimeoutException: тайм-аут соединения;ReadTimeoutException: тайм-аут чтения;ForeignKeyException: ошибка удаления сущности при нарушении целостности контролируемых внешних ссылок (integrity-check);TooManyResults: ошибка чтения сущности командойgetпри получении более одной записи по условию;IncFailException: ошибка нарушения допустимого значения при использованииinc.BufferTimeoutException: тайм-аут буферной обработки запроса.
Возможна отправка нескольких пакетов (batch запрос по спецификации JSON-RPC 2.0):
final Result result = packetClient.execute(Arrays.asList(Packet.createPacket(), Packet.createPacket()));
Пакеты такого запроса будут обрабатываться сервисом параллельно и независимо друг от друга, соответственно каждый пакет имеет собственный результат или исключение по ошибке.
Описание класса Result:
boolean isSuccess(): принимает значениеtrueесли все пакеты обработаны успешно;Collection<Error> getErrors(): коллекция ошибочных пакетов при наличии таковых. Класс элемента коллекцииError:Exception getException(): ошибка исполнения пакета;JsonSerializable getPacket(): экземпляр пакета из входящей коллекции методаexecute.
О механизме обработки batch-запросов#
Входящие в batch-запрос пакеты команд выполняются параллельно и независимо друг от друга.
Транзакционная целостность обеспечивается в границах обработки каждого отдельного пакета команд.
Каждый пакет команд в batch-запросе будет выполняться параллельно в отдельной транзакции БД.
То есть выполнение 10 пакетов в batch-запросе аналогично 10 независимым вызовам сервиса packet.
Внимание!
Так как batch-запрос выполняется целиком на одном экземпляре DataSpace, количество пакетов в batch-запросе не должно превышать размер пула потоков в DataSpace (см. настройку
batch-pool.max-threads). То есть, если количество потоков-обработчиков в пуле DataSpace равно 50, количество пакетов в одном batch-запросе не должно превышать 49.
Если количество пакетов превысит количество потоков-обработчиков, то часть из них станет в очередь, размер которой можно задать с помощью параметра batch-pool.queuesize.
Параметр dataspace-core.jsonrpc.parallelBatchProcessingTimeout задает время в миллисекундах, за которое должен выполниться пакет.
Значение по умолчанию — 500 миллисекунд.
Если пакет не успевает выполниться, в ответе будет возвращена ошибка (TimeoutException).
При этом другие пакеты из данного batch-запроса, которые выполнены без ошибок, будут иметь успешный ответ.
Mapping ошибок DataSpace на коды HTTP-статусов#
На стороне компонента DataSpace Core реализована возможность настройки кодов HTTP-статусов, которые будут использоваться в ответах при возникновении соответствующих ошибок сервиса.
Mapping осуществляется в следующем формате: <код ошибки DataSpace>:<код HTTP-статуса>.
Пример: PARSE_ERROR:505.
Коды ошибок DataSpace, доступные для mapping на HTTP-статусы:
DEFAULT — по умолчанию используется для всех ошибок, для которых явно не задан mapping;
BATCH_ERROR;
AGGREGATE_EXCEPTION;
AGGREGATE_VERSION_EXCEPTION;
APPLICATION_LOCK_EXCEPTION;
COMPARE_NOT_EQUAL;
FOREIGN_KEY;
HISTORY_EXCEPTION;
IDEMPOTENCY_EXCEPTION;
INVALID_ARGUMENT;
DATA_ACCESS;
DATA_ACCESS_CONSTRAINT;
MASK_NOT_MATCH_EXCEPTION;
OBJECT_NOT_FOUND;
PARSE_ERROR;
READ_RECORDS_COUNT_EXCEEDED_LIMIT_EXCEPTION;
SBERFLAKE_EXCEPTION;
SECURITY_EXCEPTION;
STATUS_EXCEPTION;
SYSTEM_LOCK_EXCEPTION;
TOO_MANY_REQUESTS;
BUFFER_TIMEOUT_EXCEPTION;
TOO_MANY_RESULTS;
INC_FAIL_EXCEPTION.
За mapping отвечает параметр dataspace.errorCodeToHttpStatusMappings.
Пример:
dataspace.errorCodeToHttpStatusMappings={PARSE_ERROR:505, AGGREGATE_VERSION_EXCEPTION:506, BATCH_ERROR:507, DEFAULT:500}
Пакет изменений Packet#
Генерируемый класс Packet определяет команды и их параметры для классов сущностей модели данных. Экземпляр класса Packetопределяет:
параметры выполнения пакета (идемпотентный);
вариант использования оптимистической блокировки;
команды по работе с сущностями модели;
формирование пакета в транспортном формате JSON-RPC 2.0;
разбор ответа исполнения пакета.
Создание экземпляра Packet может быть выполнено несколькими способами:
Пакет без дополнительных условий выполнения:
final Packet packet = new Packet();final Packet packet = Packet.createPacket();Идемпотентный пакет:
final Packet packet = Packet.createIdempotencyPacket("ключ_идемпотетности");Запрос версии агрегата:
final Packet packet = Packet.builder() .withAggregateVersionRequest() .build();Проверка версии агрегата:
final Packet packet = Packet.builder() .withAggregateVersion(42) .build();Сочетание идемпотентного вызова и версии агрегата:
final Packet packet = Packet.builder() .withIdempotencePacketId("ключ_идемпотентности") .withAggregateVersion(42) .build();или:
final Packet packet = Packet.builder() .withIdempotencePacketId("ключ_идемпотентности") .withAggregateVersionRequest() .build();
Экземпляр класса Packet (далее — пакет) включает свойства, соответствующие типам сущности модели данных, которые позволяют обратиться к командам для этого типа. Пример по созданию экземпляра для типа модели ProductParty:
// создание пакета
final Packet packet = Packet.createPacket();
// добавление в пакет команды создания экземпляра продукта, результат метода — ссылка на создаваемый экземпляр
final ProductPartyRef productPartyRef = packet.productParty.create(CreateProductPartyParam.create());
// вызов сервиса на исполнение пакета
dataspaceCorePacketClient().execute(packet);
// идентификатор созданной сущности
final String productPartyId = productPartyRef.getId();
Пояснение к примеру:
Создается экземпляр пакета для последующего заполнения командами.
Класс
packetсодержит свойствоproductParty, предоставляющее доступ к командам для этого типа модели.Метод
createотражает команду пакетаcreate, которая предназначена для создания экземпляра. В примере не определены значения для создаваемой сущности.Команда
createвозвращает идентификатор созданной сущности, но это событие произойдет при выполнении пакета сервером. Результат методаcreateтипаProductPartyRefявляется указателем на результат выполнения команды.Пакет передается на исполнение сервисом через клиента
dataspaceCorePacketClient().execute(packet).После успешного выполнения пакета указатель команды
productPartyRefсодержит значение идентификатора созданной сущности.
Следует отметить, что пакет может быть исполнен успешно только один раз. Свойство packet.isExecuted() примет значение true для выполненного пакета.
Другие свойства пакета заполняемые после выполнения:
isIdempotenceResponse(): принимает значениеtrueдля повторного вызова идемпотентного пакета (подробнее в разделе «Использование идемпотентности (Idempotency)»);getAggregateVersion(): версия агрегата при использовании оптимистической блокировки в пакете.
Указатель на идентификатор сущности ТипСущностиRef также используется для соблюдения строгой типизации контракта методов команд пакета.
Следующий пример демонстрирует использование команды update для изменения сущности с идентификатором 42:
// создание пакета
final Packet packet = Packet.createPacket();
// добавление в пакет команды изменения сущности с идентификатором 42
packet.productParty.update(
ProductPartyRef.of("42"),
UpdateProductPartyParam
.create()
.setCode("Новый код для продукта с ID 42")
.setName("Новое наименование для продукта с ID 42")
);
// вызов сервиса на исполнение пакета
dataspaceCorePacketClient().execute(packet);
Первый параметр метода update принимает идентификатор сущности обернутый указателем ProductPartyRef.
Второй параметр определяет значения для изменяемых свойств сущности. В примере меняются свойства code и name.
Следующий пример демонстрирует создание связанных сущностей в одном пакете:
// создание пакета
final Packet packet = Packet.createPacket();
// создание экземпляра ProductParty
final ProductPartyRef productPartyRef = packet.productParty.create(
CreateProductPartyParam.create()
);
// PerformedService является элементом агрегата ProductParty, родительское свойство 'product'
final PerformedServiceRef performedServiceRef = packet.performedService.create(p -> p
.setProduct(productPartyRef)
);
// PerformedOperation является дочерним относительно PerformedService, родительское свойство 'service'
final PerformedOperationRef performedOperationRef = packet.performedOperation.create(
CreatePerformedOperationParam
.create()
.setService(performedServiceRef));
// вызов сервиса на исполнение пакета
dataspaceCorePacketClient().execute(packet);
Пояснение к примеру:
по модели тип
ProductPartyявляется владельцемPerformedServiceчерез свойствоproduct;тип
PerformedServiceв свою очередь владеетPerformedOperationчерез свойствоservice;методы создания
PerformedServiceиPerformedOperationзаполняют родительские свойства указателями на команды создания своих родителей.
Следует отметить:
При использовании указателя на результат команды необходимо учитывать, что команда, формирующая указатель, должна быть раньше в пакете команды, использующей указатель.
Нельзя обращаться к свойству
getId()указателя до выполнения пакета, т.к. идентификатор не определен. Ошибка при таком обращении: Попытка использовать ссылку из еще не выполненного пакета.
Пакет не накладывает ограничения на количество применяемых к экземпляру команд, кроме удаляющей сущность команды delete.
Следующий пример демонстрирует создание, изменение и удаление сущности с чтением промежуточного состояния:
// создание пакета
final Packet packet = Packet.createPacket();
// создание экземпляра с указанием свойства 'name'
final ProductPartyRef productPartyRef = packet.productParty.create(
CreateProductPartyParam
.create()
.setName("Наименование после создания")
);
// чтение сущности после создания
final ProductPartyGet stateAfterCreate = packet.productParty.get(
productPartyRef,
ProductPartyWith::withName
);
// изменение свойства 'name' сущности
packet.productParty.update(
productPartyRef,
UpdateProductPartyParam
.create()
.setName("Новое наименование продукта")
);
// чтение сущности после изменения
final ProductPartyGet stateAfterUpdate = packet.productParty.get(
productPartyRef,
ProductPartyWith::withName
);
// удаление сущности
packet.productParty.delete(productPartyRef);
// вызов сервиса на исполнение пакета
dataspaceCorePacketClient().execute(packet);
assertThat(stateAfterCreate.getName()).isEqualTo("Наименование после создания");
assertThat(stateAfterUpdate.getName()).isEqualTo("Новое наименование продукта");
В примере используется команда чтения сущности get, позволяющая читать состояние сущности. Подробное описание см. в разделе «Команда чтения сущности get».
Расширение предыдущего примера показывает ошибку обработки пакета при обращении к удаленной сущности, идентификатор которой получен в результате промежуточного чтения:
// создание пакета для попытки обновления несуществующей сущности
final Packet tryUpdatePacket = Packet.createPacket();
// обновление сущности по идентификатору промежуточного чтения
tryUpdatePacket.productParty.update(
ProductPartyRef.of(stateAfterCreate.getObjectId()),
UpdateProductPartyParam.create()
);
// выполнение пакета содержит ошибку об отсутствии сущности
assertThatCode(() -> dataspaceCorePacketClient().execute(tryUpdatePacket))
.isInstanceOf(ObjectNotFoundException.class)
.hasMessage("Ошибка обработки команды id = '0', name = 'update': Не найден экземпляр типа 'ProductParty' с идентификатором '7045323725293289473'");
Сообщение об ошибке содержит id команды, который соответствует порядку добавления команды в пакет, начиная с 0.
Контекст пакета
Для удобства работы с пакетом на клиентской стороне имеется возможность задавать идентификаторы команд, задаваемых в пакете, и впоследствии обращаться к результатам выполнения команд через пакет. Можно получить как результат команды выполненного пакета, так и сослаться на еще не созданный объект, как результат команды в текущем пакете. Тип задаваемого идентификатора — строковый.
Идентификаторы можно задавать для команд типов create, updateOrCreate, tryLock, unlock.
Пример использования:
// Задаем идентификаторы для команд
final String createId = "createProductParty";
final String lockId = "lockProductParty";
final String unlockId = "unlockProductParty";
final String updateOrCreateId = "updateProductParty";
Packet packet1 = Packet.createPacket();
packet1.productParty.create(param -> param.setCode("myCode"), createId);
packet1.productParty.tryLock(
// Ссылаемся на создаваемый объект через контекст!
packet1.getResultById(createId),
param -> param.setTimeout(1000L),
lockId
);
executePacket(packet1);
Packet packet2 = Packet.createPacket();
packet2.productParty.updateOrCreate(CreateProductPartyParam.create()
// Ссылаемся на созданный объект выполненного ранее пакета через контекст!
.setId(((ProductPartyRef)packet1.getResultById(createId)).getId())
.setCode("otherCode")
.setComment("myComment"),
// Задаем идентификатор для команды updateOrCreate. Результат можно запросить позднее через обращение к пакету.
updateOrCreateId);
executePacket(packet2);
Packet packet3 = Packet.createPacket();
// Задаем идентификатор для ответа разблокировки. Результат можно запросить позднее через обращение к пакету.
packet3.productParty.unlock(
packet1.getResultById(createId),
((LockRs) packet1.getResultById(lockId)).getToken(),
unlockId);
executePacket(packet3);
Внимание!
Раздел «Транзакционная граница пакета» содержит информацию о применимости агрегатов в пакете.
Команда создания сущности create#
Команда создания сущности create доступна в группе команд типа экземпляра класса Packet.
Сигнатура на примере ProductParty:
ProductPartyRef create(CreateProductPartyParam param)
ProductPartyRef create(Consumer<CreateProductPartyParam> param)
Описание:
Генерируемый класс
CreateProductPartyParamопределяет параметры для свойств создаваемой сущности. Состав этого класса зависит от описания типа сущности в модели данных.Метод возвращает указатель идентификатора созданной сущности. Подробности рассмотрены ранее.
Пример использования:
final Packet packet = Packet.createPacket();
final ProductPartyRef productPartyRef = packet.productParty.create(
CreateProductPartyParam
.create()
.setCode("код продукта")
.setName("наименование продукта")
);
Второй вариант:
final Packet packet = Packet.createPacket();
final ProductPartyRef productPartyRef = packet.productParty
.create(p -> p
.setCode("код продукта")
.setName("наименование продукта")
);
Правила проекции свойств типа модели на атрибуты команды#
В параметры команды входят все свойства типа сущности описанные в модели данных. Имена свойств совпадают с атрибутами команды.
Свойства типа сущности с mappedBy не используются в команде.
Свойства сущности отмеченные mandatory="true", parent="true" или входящие в состав cci индекса является обязательными к заполнению.
При выполнении команды значения ссылочных свойств (кроме внешних ссылок) проверяется на существование сущности.
Если сущность не найдена, команда генерирует исключение ObjectNotFoundException.
Идентификатор сущности setId()#
Особенности:
обязательный с типом сущности, определяющим категорию идентификатора
MANUAL;опциональный с типом сущности, определяющим категорию идентификатора
AUTO_ON_EMPTYилиUUIDV4_ON_EMPTY;отсутствует с типом сущности, определяющим категорию идентификатора
AUTO(SNOWFLAKE) илиUUIDV4.
Внешние ссылки и коллекции внешних ссылок#
Информация о внешних ссылках доступна в документе «Руководство по ведению модели данных».
Для внешних ссылок генерируются классы, обеспечивающие строгую типизацию в параметрах команды.
Имя класса составляется по правилу ИмяТипаВнешнейСсылкиReference. Класс содержит статический конструктор of с одним или двумя параметрами типа String для значения внешней ссылки. Количество параметров зависит от типа ссылки:
один параметр
entityIdдля ссылки на внешний по отношению к модели тип или на корневой элемент агрегата;два параметра
entityIdиrootEntityIdдля ссылки на элемент агрегата.
Пример заполнения внешней ссылки:
final ProductPartyRef productPartyRef = packet.productParty
.create(p -> p
.setClient(ClientReference.of("client_id"))
.setExternalContract(ContractReference.of("contract_id", "contract_aggregate_id"))
);
Для внешних ссылок на типы модели данных возможно использование указателя идентификатора:
// создание контракта, который по модели является вложенным относительно корня агрегата типа
final Packet contractPacket = Packet.createPacket();
final ProductPartyRef contractOwnerProductRef = contractPacket.productParty.create(CreateProductPartyParam.create());
final PerformedServiceRef performedServiceRef = contractPacket.performedService.create(
CreatePerformedServiceParam.create().setProduct(contractOwnerProductRef)
);
final PerformedOperationRef performedOperationRef = contractPacket.performedOperation.create(
CreatePerformedOperationParam.create().setService(performedServiceRef)
);
final ContractRef contractRef = contractPacket.contract.create(
CreateContractParam.create().setPerformedOperation(performedOperationRef)
);
assertThatCode(() -> dataspaceCorePacketClient().execute(contractPacket)).doesNotThrowAnyException();
// создание продукта с внешней ссылок на ранее созданный контракт другого продукта
final Packet packet = Packet.createPacket();
final ProductPartyRef ref = packet.productParty
.create(p -> p
.setClient(ClientReference.of("client_id"))
.setExternalContract(
contractRef,
contractOwnerProductRef)
);
Параметр команды коллекции внешних ссылок использует параметризуемый класс ExternalReferenceSetModify позволяющий описать изменения коллекции:
удалить все элементы из коллекции;
добавить элементы в коллекцию;
удалить элементы из коллекции.
Класс включает несколько статических конструкторов:
ExternalReferenceSetModify<V> create(V... add): добавление в коллекцию элементов.ExternalReferenceSetModify<V> create(Set<V> add): добавление в коллекцию элементов указанных вadd.ExternalReferenceSetModify<V> create(Set<V> add, Set<V> remove): удаление элементов указанных вremove, после этого добавление элементов указанных вadd.ExternalReferenceSetModify<V> create(boolean needClear, Set<V> add, Set<V> remove): удаление всех элементов при значенииtrueпараметраneedClear, удаление элементов коллекции указанных вremove, добавление элементов коллекции указанных вadd.ExternalReferenceSetModify<V> createWithClear(V... add): удаление всех элементов в коллекции с последующим добавлением.
Примечание
Возможность удаления элементов коллекции актуальна при изменении сущности.
Пример заполнения коллекции внешних ссылок:
final Packet packet = Packet.createPacket();
final ProductPartyRef ref = packet.productParty
.create(p -> p
.setClients(ExternalReferenceSetModify
.create(
ClientReference.of("first_client_id"),
ClientReference.of("second_client_id"),
ClientReference.of("third_client_id")
)
)
);
Для применения в ExternalReferenceSetModify ссылки на созданный объект ...Ref необходимо создать ...Reference на основе этой ссылки.
Существуют два способа сделать это:
использовать метод
getRef()экземпляра...Ref(начиная с версии 1.14.0);использовать
PacketEntityUtils.getRef(...), где в качестве параметра передается экземпляр...Ref.
Оба варианта представлены в следующих примерах:
@Test
void useCaseByRefTest() {
Packet packet = Packet.createPacket();
ProductPartyRef productPartyRef = packet.productParty.create(CreateProductPartyParam.create());
packet.productParty.update(
productPartyRef,
p -> p
.setProductCheckedRefs(
ExternalReferenceSetModify
.create(ProductPartyReference.of(
productPartyRef.getRef()
)
)
)
);
ProductPartyGet productPartyGet = packet.productParty.get(
productPartyRef,
g -> g.withProductCheckedRefs(RciProductPartyProductCheckedRefsCollectionWith::withReference)
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
Optional<RciProductPartyProductCheckedRefsGet> reference = productPartyGet.getProductCheckedRefs().stream().findFirst();
assertThat(reference.isPresent()).isTrue();
assertThat(reference.get().getReference().getEntityId()).isEqualTo(productPartyRef.getId());
}
@Test
void useCasePacketEntityUtilsTest() {
Packet packet = Packet.createPacket();
ProductPartyRef productPartyRef = packet.productParty.create(CreateProductPartyParam.create());
packet.productParty.update(
productPartyRef,
p -> p
.setProductCheckedRefs(
ExternalReferenceSetModify
.create(ProductPartyReference.of(
PacketEntityUtils.getRef(productPartyRef)
)
)
) );
ProductPartyGet productPartyGet = packet.productParty.get(
productPartyRef,
g -> g.withProductCheckedRefs(RciProductPartyProductCheckedRefsCollectionWith::withReference)
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
Optional<RciProductPartyProductCheckedRefsGet> reference = productPartyGet.getProductCheckedRefs().stream().findFirst();
assertThat(reference.isPresent()).isTrue();
assertThat(reference.get().getReference().getEntityId()).isEqualTo(productPartyRef.getId());
}
Статус#
Если для типа сущности определена статусная модель, то в параметрах команды становятся доступны методы с шаблонными именами statusForКодНаблюдателя.
Параметры метода:
обязательный параметр устанавливаемого сущности статуса;
опциональный параметр описания причины перехода.
Пример заполнения статусов для двух наблюдателей:
final Packet packet = Packet.createPacket();
final ProductPartyRef ref = packet.productParty.create(p -> p
.setStatusForPlatform(ProductPartyPlatformStatus.PRODUCTCREATED)
.setStatusForService(
ProductPartyServiceStatus.DEPOSITOPENED,
"демонстрация статуса")
);
Работа с null значениями#
Существуют два варианта работы с null значениями параметров команды create и секции создания команды updateOrCreate:
null-значение передается в составе пакета;
null-значение игнорируется.
Используемый вариант определяется параметром allowNullWithCreateParam секции configuration плагина model-api-generator-maven-plugin для цели createSdk.
Значение true (дефолтное) определяет первый вариант, т.е. null-значение передается в составе пакета, иначе используется второй вариант.
Для демонстрации используется класс модели:
<class name="Sample">
<property name="name" type="String" default-value="default name"/>
</class>
Пакет создания сущности этого класса:
final Packet packet = Packet.createPacket();
final SampleRef sampleRef = packet.sample.create(p -> p.setName(null));
final SampleGet sampleGet = packet.sample.get(sampleRef, SampleWith::withName);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
log.info(">>> {}", sampleGet.getName());
Для сгенерированного SDK с параметром allowNullWithCreateParam равным true, лог-запись будет содержать >>> null.
Если изменить значение allowNullWithCreateParam на false, выполнить генерацию SDK и повторить тест, то в логе будет запись >>> default name, что соответствует значению атрибута default-value класса модели. Следует учитывать поведение при mapping pojo-объектов.
Команда создания или изменения сущности updateOrCreate#
Команда updateOrCreate выполняет поиск сущности по ключевым критериям. Если сущность найдена, выполняется ее обновление, иначе создается новый экземпляр.
Доступность команды для типа зависит от наличия одного из условий:
категория идентификатора
MANUAL;категория идентификатора
AUTO_ON_EMPTYилиUUIDV4_ON_EMPTY;для категории идентификатора
AUTO(SNOWFLAKE) илиUUIDV4обязательно наличие уникального индекса.
Примечание
Для стратегии наследования
SINGLE_TABLEучитывается доступность индексов предков. Доступность индексов предков для стратегии наследованияJOINEDопределяется параметром<allowUseParentUniqueIndex>true</allowUseParentUniqueIndex>конфигурации плагинаmodel-api-generator-maven-pluginдля целейcreateModelиcreateSdk. А также сервисdataspace-coreдолжен запускаться с моделью, сформированной с включенным параметром.
Сигнатура метода на примере ProductParty:
ProductPartyRef updateOrCreate(CreateProductPartyParam param)
Тип модели ProductParty определяет категорию идентификатора AUTO_ON_EMPTY, что позволяет применить команду updateOrCreate, но не определяет уникального индекса, поэтому возможности альтернативного поиска нет. В качестве базового параметра принимается параметр, аналогичный команде create.
Класс указателя ProductPartyRef включает атрибут isCreated(), указывающий на факт создания сущности командой, т.е. значение этого атрибута true означает создание сущности, иначе изменение сущности.
Поиск по уникальному индексу
Модель технической сущности для демонстрации:
<model>
<class name="SampleEntity">
<id category="AUTO_ON_EMPTY"/>
<property name="name" type="String"/>
<property name="altKey" type="String" unique="true"/>
</class>
</model>
Сущность SampleEntity имеет категорию идентификатора AUTO_ON_EMPTY, а также уникальный индекс по свойству altKey.
Для такой сущности возможны два варианта команды updateOrCreate:
SampleEntityRef updateOrCreate(CreateSampleEntityParam param);
SampleEntityRef updateOrCreate(CreateSampleEntityParam param, KeySampleEntity key);
Первый метод для применения команды с заполнением идентификатора. Второй метод для применения с уникальным индексом.
Тип имеет только один уникальный индекс, поэтому генерируемое перечисление KeySampleEntity содержит единственное значение ALTKEY.
Команда с использованием пользовательского значения уникального идентификатора:
final Packet packet = Packet.createPacket();
final SampleEntityRef sampleEntityRef = packet.sampleEntity.updateOrCreate(
CreateSampleEntityParam
.create()
.setId("42")
);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet)).doesNotThrowAnyException();
assertThat(sampleEntityRef.getId()).isEqualTo("42");
assertThat(sampleEntityRef.isCreated()).isTrue();
При выполнении команды используется значение уникального идентификатора.
Повторное выполнение кейса потребует изменения проверки на isCreated(), т.к. сущность будет найдена.
final Packet packet = Packet.createPacket();
final SampleEntityRef sampleEntityRef = packet.sampleEntity.updateOrCreate(
CreateSampleEntityParam
.create()
.setId("42")
);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet)).doesNotThrowAnyException();
assertThat(sampleEntityRef.getId()).isEqualTo("42");
assertThat(sampleEntityRef.isCreated()).isFalse();
В модели для типа определен уникальный индекс по свойству altKey, поэтому доступна дополнительная сигнатура команды с поиском по этому индексу.
Пример использования:
IntStream.rangeClosed(1, 2).forEach(value -> {
final Packet packet = Packet.createPacket();
final SampleEntityRef sampleEntityRef = packet.sampleEntity.updateOrCreate(
CreateSampleEntityParam
.create()
.setAltKey("KEY-42"),
KeySampleEntity.ALTKEY
);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet)).doesNotThrowAnyException();
assertThat(sampleEntityRef.isCreated()).isEqualTo(value == 1);
});
При выполнении команды будет произведен поиск по свойствам, входящим в уникальный индекс altKey.
Значение для поиска используется из параметра команды altKey. Пример показывает, что первый вызов создаст сущность, а последующий ее изменит.
Значение идентификатора и поиск по уникальному индексу
Параметр команды заполняется по правилам create, т.е. использование свойства id зависит от категории идентификатора.
Для категории AUTO_ON_EMPTY или UUIDV4_ON_EMPTY свойство является опциональным. Логика команды требует определенного критерия, который однозначно идентифицирует сущность.
Если заполнен id, то поиск выполняется по значению этого свойства. Если id не используется, то обязательно должен быть заполнен параметр
метода имени уникального ключа.
Значения для поиска определяются следующим образом:
Используется значение для свойства из состава индекса указанное в параметрах команды.
Если атрибут не указан, то используется значение по умолчанию для поля.
В противном случае — значение «null».
Для расширенной демонстрации использования уникальных индексов используется модель технического типа сущности с несколькими составными индексами:
<model>
<class name="Address" embeddable="true">
<property name="city" type="String"/>
<property name="street" type="String"/>
</class>
<class name="SampleEntity">
<property name="name" type="String" unique="true" default-value="default name"/>
<property name="address" type="Address"/>
<reference name="client" type="Client" label="Ссылка на тип все модели"/>
<reference name="service" type="PerformedService" label="Ссылка на элемент агрегата текущей модели"/>
<index unique="true">
<property name="address.city"/>
</index>
<index unique="true">
<property name="client"/>
</index>
<index unique="true">
<property name="name"/>
<property name="address"/>
</index>
<index unique="true">
<property name="service"/>
</index>
</class>
</model>
Модель сущности приведена исключительно в демонстрационных целях. Тип имеет следующие уникальные индексы:
свойство
name;свойство
cityвложенного объектаAddress;свойство внешней ссылки
clientна объект вне модели;свойства
name,addressсоставляют составной уникальный индекс;свойство внешней ссылки
serviceна объект элемента агрегата текущей модели.
Имя индекса составляется из имен входящих в него свойств, объединенных символом _. Составные свойства (вложенные объекты и внешние ссылки) включаются в индекс полным составом полей с объединением через символ __. Включенное в индекс свойство составного типа (например, address.city) включается с заменой символа . на символ __.
Таким образом для демонстрационной сущности определены следующие имена индексов:
name;address__city;client__entityId;name_address__city_address__street;service__entityId_service__rootEntityId.
После генерации модели для типа сущности сигнатура updateOrCreate имеет вид:
SampleEntityRef updateOrCreate(CreateSampleEntityParam param, KeySampleEntity key)
Параметр key является генерируемым enum для доступных типу уникальных индексов:
public enum KeySampleEntity {
ADDRESS__CITY("address__city"),
CLIENT__ENTITYID("client__entityId"),
NAME("name"),
NAME_ADDRESS__CITY_ADDRESS__STREET("name_address__city_address__street"),
SERVICE__ENTITYID_SERVICE__ROOTENTITYID("service__entityId_service__rootEntityId")
// ...
}
Пример применения команды в сочетании с индексом по свойству внешней ссылки client:
IntStream.rangeClosed(1, 2).forEach(value -> {
final Packet packet = Packet.createPacket();
final SampleEntityRef sampleEntityRef = packet.sampleEntity.updateOrCreate(
CreateSampleEntityParam
.create()
.setClient(ClientReference.of("client_id")),
KeySampleEntity.CLIENT__ENTITYID
);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet)).doesNotThrowAnyException();
assertThat(sampleEntityRef.isCreated()).isEqualTo(value == 1);
});
Значение внешней ссылки передано в параметрах команды setClient(ClientReference.of("client_id")).
Пример применения команды в сочетании со значением по умолчанию:
IntStream.rangeClosed(1, 2).forEach(value -> {
final Packet packet = Packet.createPacket();
final SampleEntityRef sampleEntityRef = packet.sampleEntity.updateOrCreate(
CreateSampleEntityParam.create(),
KeySampleEntity.NAME
);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet)).doesNotThrowAnyException();
assertThat(sampleEntityRef.isCreated()).isEqualTo(value == 1);
});
Модель типа для свойства name определяет значение по умолчанию default name. В примере параметры не имеют явного указания значения этого свойства, но второй вызов метода не создаст новую сущность, т.к. существует запись со значением по умолчанию, которое будет использовано при формировании критерия поиска.
Частичное обновление сущности
Выполнение команды для существующей сущности приводит к изменению текущих значений полей на указанные в параметрах команды.
Частичное обновление полей можно выполнить с использованием параметра команды exist, класс которого генерируется в соответствии с типом сущности.
Имя типа имеет маску ExistИмяСущностиParam, например, для ProductParty класс параметра будет называться ExistProductPartyParam.
Класс параметра всегда включает метод setUpdate, принимающий значение изменяемых полей.
Если тип модели сущности допускает использование команды с дополнительными ключами, то класс параметра включает метод setByKey для определения ключа согласно ранее описанному в разделе.
Модель технической сущности для демонстрации:
<model>
<class name="SampleEntity">
<id category="AUTO_ON_EMPTY"/>
<property name="code" type="String"/>
<property name="name" type="String"/>
<property name="altKey" type="String" unique="true"/>
</class>
</model>
Пример частичного изменения сущности при повторном вызове метода:
final String key = uuid();
IntStream.rangeClosed(1, 2).forEach(useCase -> {
final Packet packet = Packet.createPacket();
final SampleRef sampleRef = packet.sample.updateOrCreate(
CreateSampleParam.create()
.setId(key)
.setCode("1")
.setName("1"),
ExistSampleParam.create()
.setUpdate(UpdateSampleParam.create()
.setName("2")
)
);
final SampleGet sampleGet = packet.sample.get(sampleRef, g -> g.withCode().withName());
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(sampleRef.isCreated()).isEqualTo(useCase == 1);
assertThat(sampleGet.getCode()).isEqualTo("1");
assertThat(sampleGet.getName()).isEqualTo(useCase == 1 ? "1" : "2");
});
В примере демонстрируется, что поле name изменено на значение 2 при вызове команды на существующей сущности, при этом значение поля code не изменилось.
В варианте с альтернативным ключом пример выглядит следующим образом:
final String key = uuid();
IntStream.rangeClosed(1, 2).forEach(useCase -> {
final Packet packet = Packet.createPacket();
final SampleRef sampleRef = packet.sample.updateOrCreate(
CreateSampleParam.create()
.setAltKey(key)
.setCode("1")
.setName("1"),
ExistSampleParam.create()
.setByKey(KeySample.ALTKEY)
.setUpdate(UpdateSampleParam.create()
.setName("2")
)
);
final SampleGet sampleGet = packet.sample.get(sampleRef, g -> g.withCode().withName());
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(sampleRef.isCreated()).isEqualTo(useCase == 1);
assertThat(sampleGet.getCode()).isEqualTo("1");
assertThat(sampleGet.getName()).isEqualTo(useCase == 1 ? "1" : "2");
});
Если при выполнении команды не требуется выполнять обновление, то необходимо в параметре для setUpdate не заполнять поля или оставить пустым consumer. Следующий пример демонстрирует оба варианта:
final String key = uuid();
final Packet packet = Packet.createPacket();
final SampleRef sampleRef = packet.sample.create(CreateSampleParam.create()
.setId(key)
.setCode("1")
.setName("1")
);
packet.sample.updateOrCreate(
CreateSampleParam.create().setId(key).setCode("2").setName("2"),
ExistSampleParam.create()
.setUpdate(UpdateSampleParam.create())
);
packet.sample.updateOrCreate(
CreateSampleParam.create().setId(key).setCode("2").setName("2"),
ExistSampleParam.create()
.setUpdate(updateSampleParam -> { })
);
final SampleGet sampleGet = packet.sample.get(sampleRef, g -> g.withCode().withName());
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(sampleGet.getCode()).isEqualTo(sampleGet.getCode()).isEqualTo("1");
assertThat(sampleGet.getCode()).isEqualTo(sampleGet.getName()).isEqualTo("1");
Команда создания иерархии объектов по образцу createBy#
Команда createBy получает на вход спецификацию набора связанных объектов, по которой создает новый набор объектов с копированием всех свойств, кроме:
идентификаторов сущностей, которые для создаваемых объектов устанавливаются по правилам, определенным в модели;
тех свойств, значения которых явно переопределены в спецификации. Для задания значений могут быть использованы «Строковые выражения»;
ссылок на создаваемые объекты в пределах агрегата — эти ссылки заменяются ссылками на созданные в операции объекты;
ссылок на объекты исходного агрегата, которые не входят в набор создаваемых объектов и принадлежат другому агрегату;
истории сущностей и статусов.
Команда createBy работает только с классами модели, у которых имеется атрибут cloneable="true".
Атрибут "cloneable" не наследуется потомками — необходимо явно задавать признак на каждом классе модели, где требуется.
На первом неабстрактном классе иерархии наследования, в которой встречается cloneable="true", создается свойство sysCloneOrigin типа String для хранения идентификатора сущности, с которой был скопирован объект.
Важно учитывать:
при работе команды выполняются все действующие ограничения для типа сущности (обязательность заполнения полей и идентификаторов, уникальность значений и т.п.);
при использовании команды для корней агрегатов или на множестве объектов, относящихся к разным экземплярам агрегатов, необходимо выполнить настройку по расширению транзакционной границы пакета;
если команда является первой в идемпотентном пакете и выполняется для корневых сущностей агрегатного типа, критерии идемпотентного вызова будут ассоциированы с первой созданной командой копией корня агрегата;
если в выборке объектов типа сущности, имеющего наследников, будет обнаружен экземпляр класс модели, которого не имеет
cloneable="true", выполнение завершится ошибкой;для корневой спецификации обязательно использование одного из методов определения источника (
sourceIdилиfilter). Для вложенных сущностей эти критерии являются опциональными, условие отбора включает поле владельца автоматически;использование большого количества объектов в команде может привести к исчерпанию ресурсов сервиса и/или переполнению размера вектора изменений.
В демонстрационной модели атрибут cloneable="true" определен для классов ProductParty, PerformedService, DepositCpt (один из наследников PerformedService), PerformedOperation, ProductLink.
Сигнатура команды createBy на примере типа ProductParty:
public CreateByRef createBy(Consumer<CreateByProductPartyParam> createByProductPartyParam)
Тип возвращаемого значения CreateByRef содержит методы:
List<String> getIds()— список созданных корневых объектов;boolean isEmptyResult()— возвращаетtrue, если команда содержит пустой результат;String commandFirstElementPath()— возвращает ссылку на команду для связывания внутри пакета. Значение может быть использовано только для еще не исполненного пакета. В поле связанной команды будет передана ссылка первого экземпляра, созданного командойcreateBy.
Тип параметра CreateByProductPartyParam содержит:
filter(Function<ProductPartyGrasp, ConditionWrapper> condition)— для определения критерия выборки сущностей для копирования;sorting(Consumer<AdvancedSortBuilder<ProductPartyGrasp>> sorting)— критерий сортировки результата выборки;sourceId(ProductPartyRef sourceRef)— определение сущности для копирования через ее идентификатор, может использоваться как альтернативаfilterпри работе с одним экземпляром источника или совместно для уточнения критерия необходимости копирования уточняющим условием изfilter;override(Consumer<OverrideProductPartyParam> consumer)— переопределение значений полей. Структура классаOverride...Paramзависит от типа сущности команды, похожа на структуру аналогичного класса командыcreate, за исключением полей с признакомparent=true, и расширенной методамиexprдля применения строковых выражений и инициализации начальных статусов;createBy(Consumer<NestedProductPartyParam> consumer)— параметризация условий копирования сущностей, для которых текущая сущность является владельцем. Если условия не определены, то обработка сущностей выполнена не будет. Наличие методаcreateByи состав классаNester...Paramзависит от структуры полей отношений «один к одному» и «один ко многим» типа сущности команды. Поле отношения будет использовано, если тип сущности значения этого поля или хотя бы один из его наследников имеет атрибутcloneable="true".
Пример использования команды с одной сущностью:
@Test
void test() {
final String sourceCode = uuid();
final Packet packet = Packet.createPacket();
final ProductPartyRef sourceRef = packet.productParty.create(p -> p.setCode(sourceCode));
final CreateByRef createByRef = packet.productParty.createBy(p -> p.sourceId(sourceRef));
final ProductPartyGet copyGet = packet.productParty.get(
ProductPartyRef.of(createByRef.commandFirstElementPath()),
g -> g
.withCode()
.withSysCloneOrigin()
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(createByRef.isEmptyResult()).isFalse();
assertThat(createByRef.getIds()).containsExactlyInAnyOrder(copyGet.getObjectId());
assertThat(copyGet.getCode()).isEqualTo(sourceCode);
assertThat(copyGet.getSysCloneOrigin()).isEqualTo(sourceRef.getId());
}
Создание и копирование выполняется в одном пакете для упрощения. В примере выше:
для команды
createByприменяется методsourceIdдля определения объекта источника, созданного ранее командойcreate;для команды
getприменяется методcommandFirstElementPath()результата командыcreateByсвязывания внутри пакета;assertThat(createByRef.isEmptyResult()).isFalse()указывает, что результат командыcreateByне пустой;assertThat(createByRef.getIds()).containsExactlyInAnyOrder(copyGet.getObjectId())указывает, что создана одна сущность, и ее идентификатор совпадает с результатом чтения;assertThat(copyGet.getCode()).isEqualTo(sourceCode)указывает, что в созданном объекте скопировано значение свойстваcode;assertThat(copyGet.getSysCloneOrigin()).isEqualTo(sourceRef.getId())указывает, что в созданном объекте свойствоsysCloneOriginзаполнено значением идентификатора исходного объекта.
Следующий пример демонстрирует использование методов sourceId и filter, где фильтр задает заведомо пустой результат поиска:
@Test
void test() {
final Packet packet = Packet.createPacket();
final ProductPartyRef sourceRef = packet.productParty.create(CreateProductPartyParam.create());
final CreateByRef createByRef = packet.productParty.createBy(p -> p
.sourceId(sourceRef)
.filter(f -> f.codeEq(uuid()))
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(createByRef.isEmptyResult()).isTrue();
assertThat(createByRef.getIds()).isEmpty();
}
Результат выполнения команды createByRef.isEmptyResult() будет true, коллекция идентификаторов getIds() — пустая.
Пример использования фильтра:
@Test
void test() {
final String sourceCode = uuid();
final Packet packet = Packet.createPacket();
final ProductPartyRef sourceRef = packet.productParty.create(p -> p.setCode(sourceCode));
final CreateByRef createByRef = packet.productParty.createBy(p -> p
.filter(f -> f.codeEq(sourceCode))
);
final ProductPartyGet copyGet = packet.productParty.get(
ProductPartyRef.of(createByRef.commandFirstElementPath()),
g -> g
.withCode()
.withSysCloneOrigin()
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(createByRef.isEmptyResult()).isFalse();
assertThat(createByRef.getIds()).containsExactlyInAnyOrder(copyGet.getObjectId());
assertThat(copyGet.getCode()).isEqualTo(sourceCode);
assertThat(copyGet.getSysCloneOrigin()).isEqualTo(sourceRef.getId());
}
Следующий пример показывает работу с несколькими исходными сущностями и применением сортировки для определения порядка идентификаторов созданных сущностей:
@Test
void test() {
final String prefix = uuid();
final Map<String, ProductPartyRef> sourceRefs = new HashMap<>();
final Packet createPacket = Packet.createPacket();
IntStream
.rangeClosed(1, 2)
.forEach(index -> {
final String currentCode = prefix + "-" + index;
final ProductPartyRef ref = createPacket.productParty.create(p -> p
.setCode(currentCode)
);
sourceRefs.put(currentCode, ref);
}
);
assertThatCode(() -> packetClient.execute(createPacket)).doesNotThrowAnyException();
final Packet packet = Packet.createPacket();
final CreateByRef createByRef = packet.productParty.createBy(p -> p
.filter(f -> f.codeLike(prefix + "%"))
.sorting(s -> s.desc(ProductPartyGrasp::code))
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(createByRef.getIds()).hasSize(2);
IntStream.rangeClosed(1, 2).forEach(index -> {
final String copyId = createByRef.getIds().get(index - 1);
final Packet getPacket = Packet.createPacket();
final ProductPartyGet copyGet = getPacket.productParty.get(
ProductPartyRef.of(copyId),
g -> g
.withCode()
.withSysCloneOrigin()
);
assertThatCode(() -> packetClient.execute(getPacket)).doesNotThrowAnyException();
assertThat(copyGet.getCode()).isEqualTo(prefix + "-" + (3 - index));
final ProductPartyRef sourceRef = sourceRefs.get(copyGet.getCode());
assertThat(sourceRef).isNotNull();
assertThat(copyGet.getSysCloneOrigin()).isEqualTo(sourceRef.getId());
});
}
Демонстрация использования override для изменения значений копируемой сущности:
@Test
void test() {
final String sourceCode = uuid();
final String sourceName = uuid();
final String sourceCoordinateName = uuid();
final String sourceCoordinateDescription = uuid();
final String copyName = uuid();
final Packet packet = Packet.createPacket();
final ProductPartyRef sourceRef = packet.productParty.create(p -> p
.setCode(sourceCode)
.setName(sourceName)
.setCoordinate(emb -> emb
.setName(sourceCoordinateName)
.setDescription(sourceCoordinateDescription)
)
.setStatusForPlatform(ProductPartyPlatformStatus.PRODUCTCHECK)
.setStatusForService(ProductPartyServiceStatus.DEPOSITCHECK)
);
final CreateByRef createByRef = packet.productParty.createBy(p -> p
.sourceId(sourceRef)
.override(override -> override
.setName(copyName)
.initStatusForPlatform()
.expr(
"id",
expr -> GraspHelper.valueOf("COPY-").concat(expr.id())
)
.expr(
"coordinate.name",
expr -> GraspHelper.valueOf("COPY-").concat(expr.coordinate().name())
)
)
);
final ProductPartyGet copyGet = packet.productParty.get(
ProductPartyRef.of(createByRef.commandFirstElementPath()),
g -> g
.withCode()
.withName()
.withCoordinate(gg -> gg
.withName()
.withDescription()
)
.withStatusForPlatform(StatusWithLinkable::withCode)
.withStatusForService(StatusWithLinkable::withCode)
.withSysCloneOrigin()
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(copyGet.getObjectId()).isEqualTo("COPY-" + sourceRef.getId());
assertThat(copyGet.getSysCloneOrigin()).isEqualTo(sourceRef.getId());
assertThat(copyGet.getCode()).isEqualTo(sourceCode);
assertThat(copyGet.getName()).isEqualTo(copyName);
assertThat(copyGet.getCoordinate().getName()).isEqualTo("COPY-" + sourceCoordinateName);
assertThat(copyGet.getCoordinate().getDescription()).isEqualTo(sourceCoordinateDescription);
assertThat(copyGet.getStatusForPlatform().getCode()).isEqualTo(ProductPartyPlatformStatus.PRODUCTCREATED.getValue());
assertThat(copyGet.getStatusForService().getCode()).isEqualTo(ProductPartyServiceStatus.DEPOSITCHECK.getValue());
}
Описание примера выше:
поля
code,statusForServiceмdescriptionвложенного объектаcoordinateкопируются без изменений;поле
nameзаменяется фиксированным значением изcopyName;поле
statusForPlatformметодомinitStatusForPlatform()принимает начальное значение согласно статусной модели;идентификатор созданной сущности вычисляется как сумма
COPY-и значения идентификатора исходной сущности, для этого используетсяexpr("id", expr -> GraspHelper.valueOf("COPY-").concat(expr.id()));поле
nameвложенного объектаcoordinateменяется на суммуCOPY-и значения этого поля в исходной сущности, для этого используетсяexpr("coordinate.name", expr -> GraspHelper.valueOf("COPY-").concat(expr.coordinate().name())).
Использование copyBy для копирования связанных сущностей:
@Test
void test() {
final Packet packet = Packet.createPacket();
final ProductPartyRef sourceProductRef = packet.productParty.create(CreateProductPartyParam.create());
packet.productLink.create(p -> p.setOwner(sourceProductRef));
final PerformedServiceRef sourceServiceRef = packet.performedService.create(p -> p.setProduct(sourceProductRef));
final PerformedOperationRef sourceOperationRef = packet.performedOperation.create(p -> p.setService(sourceServiceRef));
final String copyCode = uuid();
final CreateByRef createByRef = packet.productParty.createBy(p -> p
.sourceId(sourceProductRef)
.override(overrideProduct -> overrideProduct.setCode(copyCode))
.createBy(nestedProduct -> nestedProduct
.performedServices(routeService -> routeService
.add(createByService -> createByService
.override(overrideService -> overrideService.setCode(copyCode))
.createBy(nestedService -> nestedService
.performedOperations(routeOperation -> routeOperation
.add(createByOperation -> createByOperation
.override(overrideOperation -> overrideOperation.setCode(copyCode))
)
)
)
)
)
)
);
final ProductPartyGet copyGet = packet.productParty.get(
ProductPartyRef.of(createByRef.commandFirstElementPath()),
g -> g
.withCode()
.withSysCloneOrigin()
.withRelatedProducts()
.withPerformedServices(gs -> gs
.withCode()
.withSysCloneOrigin()
.withPerformedOperations(go -> go
.withCode()
.withSysCloneOrigin()
)
)
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(copyGet.getCode()).isEqualTo(copyCode);
assertThat(copyGet.getSysCloneOrigin()).isEqualTo(sourceProductRef.getId());
assertThat(copyGet.getRelatedProducts()).isEmpty();
assertThat(copyGet.getPerformedServices()).hasSize(1);
final PerformedServiceGet copyServiceGet = copyGet.getPerformedServices().stream().findFirst().orElse(null);
assertThat(copyServiceGet).isNotNull();
assertThat(copyServiceGet.getCode()).isEqualTo(copyCode);
assertThat(copyServiceGet.getSysCloneOrigin()).isEqualTo(sourceServiceRef.getId());
assertThat(copyServiceGet.getPerformedOperations()).hasSize(1);
final PerformedOperationGet copyOperationGet = copyServiceGet.getPerformedOperations().stream().findFirst().orElse(null);
assertThat(copyOperationGet).isNotNull();
assertThat(copyOperationGet.getCode()).isEqualTo(copyCode);
assertThat(copyOperationGet.getSysCloneOrigin()).isEqualTo(sourceOperationRef.getId());
}
В примере выше исходный экземпляр продукта включает экземпляры сервиса и связанного продукта, сервис в свою очередь включает операцию.
В команде createBy определяется копирование вложенности продукт -> сервис -> операция, исключая связанные продукты.
При копировании для всех объектов поле code меняется на фиксированное значение. В методе createBy корневой сущности имеются два метода для перехода на связанные сущности — performedServices и relatedProducts.
По условию примера копируются только связанные сервисы, поэтому метод relatedProducts не используется.
Параметр performedServices представляет переход в коллекцию связанных сущностей, для которой можно применить различные варианты копирования элементов с самостоятельной фильтрацией, переопределением параметров и переходом на связанные сущности.
Добавление условий выполняется методом add, параметр которого представляет структуру команды createBy для типа связанной сущности.
Элементами коллекции могут быть наследники от базового типа коллекции, для доступа к их свойствам применяется метод addCast с типом наследника.
Следующий пример показывает работу с наследниками в коллекции:
@Test
void test() {
final Packet packet = Packet.createPacket();
final ProductPartyRef sourceProductRef = packet.productParty.create(CreateProductPartyParam.create());
final PerformedServiceRef sourceServiceRef = packet.performedService.create(p -> p.setProduct(sourceProductRef));
final DepositCptRef sourceDepositCptRef = packet.depositCpt.create(p -> p.setProduct(sourceProductRef));
final String copyCode = uuid();
final String copyChannel = uuid();
final CreateByRef createByRef = packet.productParty.createBy(p -> p
.sourceId(sourceProductRef)
.createBy(nested -> nested
.performedServices(route -> route
.add(createBy -> createBy
.filter(f -> f.typeEq("PerformedService"))
.override(override -> override
.setCode(copyCode)
)
)
.addCastDepositCpt(createBy -> createBy
.override(override -> override
.setChannel(copyChannel)
)
)
)
)
);
final ProductPartyGet copyGet = packet.productParty.get(
ProductPartyRef.of(createByRef.commandFirstElementPath()),
g -> g
.withSysCloneOrigin()
.withPerformedServices(gg -> gg
.withCode()
.withSysCloneOrigin()
.withType()
.setWhere(w -> w.typeEq("PerformedService"))
)
.withPerformedServices(
"depositCpt",
PerformedServiceCollectionWithPicker::DepositCpt,
gg -> gg
.withCode()
.withChannel()
.withSysCloneOrigin()
)
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(copyGet.getSysCloneOrigin()).isEqualTo(sourceProductRef.getId());
assertThat(copyGet.getPerformedServices()).hasSize(1);
final PerformedServiceGet copyServiceGet = copyGet.getPerformedServices().stream().findFirst().orElse(null);
assertThat(copyServiceGet).isNotNull();
assertThat(copyServiceGet.getSysCloneOrigin()).isEqualTo(sourceServiceRef.getId());
assertThat(copyServiceGet.getCode()).isEqualTo(copyCode);
final GraphCollection<DepositCptGet> depositCpt = copyGet.getPerformedServices(
"depositCpt",
PerformedServiceGetPicker::DepositCpt
);
assertThat(depositCpt).hasSize(1);
final DepositCptGet copyDepositCptGet = depositCpt.stream().findFirst().orElse(null);
assertThat(copyDepositCptGet).isNotNull();
assertThat(copyDepositCptGet.getSysCloneOrigin()).isEqualTo(sourceDepositCptRef.getId());
assertThat(copyDepositCptGet.getCode()).isNull();
assertThat(copyDepositCptGet.getChannel()).isEqualTo(copyChannel);
}
В примере выше в исходном продукте через performedService доступны два экземпляра сервисов, один — с типом PerformedService, второй — с типом DepositCpt.
В спецификации создания копий по общему типу указан фильтр .filter(f -> f.typeEq("PerformedService")) для исключения наследников.
Для получения доступа к специфичному полю channel типа DepositCpt выполнено добавление условия копирования для этого типа методом addCastDepositCpt.
Следующий пример показывает ошибку, которую можно получить при работе с коллекциями наследуемых сущностей, если не фильтровать по клонируемым типам:
@Test
void test() {
final Packet packet = Packet.createPacket();
final ProductPartyRef sourceProductRef = packet.productParty.create(CreateProductPartyParam.create());
packet.performedService.create(p -> p.setProduct(sourceProductRef));
packet.depositCpt.create(p -> p.setProduct(sourceProductRef));
packet.depositOpenSrvCBExmpl.create(p -> p
.setProduct(sourceProductRef)
.setContractLogicMandatory(SuperContractReference.of(uuid(), uuid()))
);
packet.productParty.createBy(p -> p
.sourceId(sourceProductRef)
.createBy(nested -> nested
.performedServices(route -> route
.add(createBy -> {
})
)
)
);
assertThatCode(() -> packetClient.execute(packet)).
hasMessageContaining("name = 'createBy': Type 'DepositOpenSrvCBExmpl' is not cloneable - createBy is not allowed for this type.");
}
В примере выше в исходной сущности имеется экземпляр типа DepositOpenSrvCBExmpl, который является наследником PerformedService, но не доступен для команды createBy.
Выполнение команды завершится ошибкой. Сервис dataspace-core содержит параметр dataspace.packet.clone.noncloneable со значением fail, определяющий вызов ошибки в такой ситуации.
Если изменить значение этого параметра на skip, то необрабатываемые сущности будут исключаться из результата.
Пример использования команды для типа, не являющегося корнем агрегата:
@Test
void test() {
final String prefix = uuid();
final Packet packet = Packet.createPacket();
IntStream.rangeClosed(1, 2).forEach(index -> {
final String currentKey = prefix + "-" + index;
final ProductPartyRef sourceProductRef = packet.productParty.create(p -> p.setId(currentKey));
packet.performedService.create(p -> p.setProduct(sourceProductRef).setCode(currentKey));
});
final CreateByRef createByRef = packet.performedService.createBy(p -> p
.filter(f -> f.code().like(prefix + "%"))
.override(override -> override
.expr(
"code",
expr -> GraspHelper.valueOf("COPY-").concat(expr.code())
)
)
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
IntStream.rangeClosed(1, 2).forEach(index -> {
final String currentKey = prefix + "-" + index;
final Packet getPacket = Packet.createPacket();
final ProductPartyGet get = getPacket.productParty.get(
ProductPartyRef.of(currentKey),
g -> g.withPerformedServices(PerformedServiceCollectionWith::withCode)
);
assertThatCode(() -> packetClient.execute(getPacket)).doesNotThrowAnyException();
assertThat(get.getPerformedServices()).hasSize(2);
assertThat(get.getPerformedServices().stream().anyMatch(ps -> ps.getCode().equals(currentKey))).isTrue();
assertThat(get.getPerformedServices().stream().anyMatch(ps -> ps.getCode().equals("COPY-" + currentKey))).isTrue();
});
}
При копировании поля, содержащие ссылки на сущности, не являющиеся справочниками, обрабатываются следующим образом:
если поле содержит ссылку на копируемый в команде объект, то значение поля заменяется новым объектом;
если поле содержит ссылку на объект, который в результате выполнения команды относится к другому агрегату (исходному), то значение поля ставится в «null». Следует учитывать это при работе с обязательными полями для избежания ошибки.
Пример обработки ссылочных полей:
@Test
void test() {
final Packet packet = Packet.createPacket();
final ProductPartyRef sourceProductRef = packet.productParty.create(CreateProductPartyParam.create());
final ProductLinkRef sourceProductLinkRef = packet.productLink.create(p -> p
.setOwner(sourceProductRef)
.setProduct(sourceProductRef)
);
final PerformedServiceRef sourceServiceRef = packet.performedService.create(p -> p
.setProduct(sourceProductRef)
);
final PerformedOperationRef sourceOperationRef = packet.performedOperation.create(p -> p
.setService(sourceServiceRef)
);
packet.performedService.update(
sourceServiceRef,
p -> p.setHistOper(sourceOperationRef)
);
final CreateByRef createByRef = packet.productParty.createBy(p -> p
.sourceId(sourceProductRef)
.createBy(nestedProduct -> nestedProduct
.relatedProducts(routeProductLink -> routeProductLink
.add(createByProductLink -> {
})
)
.performedServices(routeService -> routeService
.add(createByService -> createByService
.sourceId(sourceServiceRef)
)
)
)
);
final ProductPartyGet copyGet = packet.productParty.get(
ProductPartyRef.of(createByRef.commandFirstElementPath()),
g -> g
.withSysCloneOrigin()
.withRelatedProducts(gg -> gg
.withSysCloneOrigin()
.withProduct(ProductPartyWithLinkable::withSysCloneOrigin)
)
.withPerformedServices(gg -> gg
.withSysCloneOrigin()
.withHistOper()
.withPerformedOperations()
)
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(copyGet.getSysCloneOrigin()).isEqualTo(sourceProductRef.getId());
assertThat(copyGet.getRelatedProducts()).hasSize(1);
final ProductLinkGet copyProductLinkGet = copyGet.getRelatedProducts().stream().findFirst().orElse(null);
assertThat(copyProductLinkGet).isNotNull();
assertThat(copyProductLinkGet.getSysCloneOrigin()).isEqualTo(sourceProductLinkRef.getId());
assertThat(copyProductLinkGet.getProduct()).isNotNull();
assertThat(copyProductLinkGet.getProduct().getSysCloneOrigin()).isEqualTo(sourceProductRef.getId());
assertThat(copyGet.getPerformedServices()).hasSize(1);
final PerformedServiceGet copyServiceGet = copyGet.getPerformedServices().stream().findFirst().orElse(null);
assertThat(copyServiceGet).isNotNull();
assertThat(copyServiceGet.getPerformedOperations()).isEmpty();
assertThat(copyServiceGet.getHistOper()).isNull();
}
В примере выше исходный продукт:
содержит экземпляр типа
ProductLink, в котором в полеproductсодержится ссылка на копируемый продукт;содержит экземпляр сервиса
PerformedService, который в свою очередь содержит экземпляр операцииPerformedOperation, и значение поляhistOperсервиса является ссылкой на операцию.
В примере копирование продукта имеет спецификацию:
копировать
ProductLink;копировать конкретный экземпляр сервиса
PerformedServiceбез его операций.
В результате копирования поле product скопированного ProductLink будет ссылаться на копию продукта, а значение поля histOper
сервиса установлено в «null», т.к. связанная с этим полем операция исключается из копирования.
Команда изменения сущности update#
Команда позволяет изменить сущность по ее идентификатору. Параметры команды зависят от описания типа сущности.
Заполнение параметров аналогично методу create с рядом особенностей:
можно указать
nullзначение для свойства с последующей записью в базу;свойство родителя может быть изменено в пределах агрегата изменяемой сущности;
сохранение коллекции примитивных значений выполняется полной заменой;
изменение выполняется только по указанным в параметрах команды свойствам;
обязательные свойства могут не указываться, но если такое свойство присутствует, то значение должно отличаться от
null;идентификатор сущности не меняется.
Для демонстрации команды используется техническая сущность:
<model>
<class name="SampleEntity">
<id category="AUTO_ON_EMPTY"/>
<property name="code" type="String"/>
<property name="name" type="String"/>
<property name="counter" type="Integer"/>
<property name="sum" type="BigDecimal"/>
</class>
</model>
Сигнатуры команды update:
void update(SampleEntityRef sampleEntity, UpdateSampleEntityParam param);
update(SampleEntityRef sampleEntity, Consumer<UpdateSampleEntityParam> param);
Параметры команды UpdateSampleEntityParam генерируются на основе описания типа сущности и схожи по структуре с параметрами команды create.
Для отсутствующей сущности команда генерирует исключение ObjectNotFoundException.
Пример ошибочной обработки:
final Packet packet = Packet.createPacket();
packet.sampleEntity.update(
SampleEntityRef.of("42"),
UpdateSampleEntityParam.create()
);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet))
.isInstanceOf(ObjectNotFoundException.class)
.hasMessage("Ошибка обработки команды id = '0', name = 'update': Не найден экземпляр типа 'SampleEntity' с идентификатором '42'");
Команда update предоставляет дополнительные возможности для сущности при наличии в ней свойств некоторых типов:
Использование compare: для свойств сущности с типами String, Integer, Long, Date, LocalDate, LocalDateTime, OffsetDateTime можно определить проверку на соответствие фактических значений сущности ожидаемым. Если по одному из указанных свойств значение не совпадает, то команда завершится ошибкой.
Проверка выполняется до внесения изменений по свойствам. В compare может быть использовано системное свойство lastChangeDate, которое становится доступным после генерации модели/SDK плагином model-api-generator-maven-plugin с параметром конфигурации <allowLastChangeDateCompare>true</allowLastChangeDateCompare>.
Использование inc: для свойств сущности с типами Integer, Long, Float, Double, BigDecimal можно выполнить операцию инкремента текущего значения на указанное в параметрах команды.
Передаваемое значение может быть отрицательным для выполнения операции декремента.
Работа с compare и inc осуществляется методом:
void update(SampleEntityRef sampleEntity, UpdateSampleEntityReq req)
Класс UpdateSampleEntityReq является агрегатором для:
UpdateSampleEntityParam: параметры изменения сущности;CompareSampleEntityParam: свойства для сравнения;IncSampleEntityParam: параметры для выполнения инкремента свойств сущности.
Классы CompareSampleEntityParam и IncSampleEntityParam зависят от структуры сущности, поэтому UpdateSampleEntityReq может включать как оба типа, так и один из них.
Если структура класса не допускает использования compare или inc, метода update с UpdateТипСущностиReq сформировано не будет.
Пример использования:
final Packet packet = Packet.createPacket();
// создание сущности с инициализирующими значениями свойств
final SampleEntityRef sampleEntityRef = packet.sampleEntity.create(
CreateSampleEntityParam
.create()
.setCode("initial code")
.setName("initial name")
.setCounter(1)
.setSum(BigDecimal.TEN)
);
// изменение сущности
packet.sampleEntity.update(
sampleEntityRef,
UpdateSampleEntityReq.create()
.setParam(
// новые значения свойств
UpdateSampleEntityParam
.create()
.setCode("new code value")
.setName("new name value")
)
// проверка, что текущие значения соответствуют ожидаемым
.setCompare(CompareSampleEntityParam
.create()
.setCode("initial code")
.setName("initial name")
)
// увеличение текущих значений на указанную величину
.setInc(IncSampleEntityParam
.create()
.setCounter(42)
.setSum(BigDecimal.valueOf(123.45))
)
);
// чтение состояния сущности после изменений
final SampleEntityGet sampleEntityGet = packet.sampleEntity.get(
sampleEntityRef,
g -> g
.withName()
.withCode()
.withCounter()
.withSum()
);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet)).doesNotThrowAnyException();
assertThat(sampleEntityGet.getCode()).isEqualTo("new code value");
assertThat(sampleEntityGet.getName()).isEqualTo("new name value");
assertThat(sampleEntityGet.getCounter()).isEqualTo(42 + 1);
assertThat(sampleEntityGet.getSum()).isEqualByComparingTo(BigDecimal.TEN.add(BigDecimal.valueOf(123.45)));
Пример ошибки сравнения:
final Packet packet = Packet.createPacket();
final SampleEntityRef sampleEntityRef = packet.sampleEntity.create(CreateSampleEntityParam
.create()
.setCode("code")
.setName("name")
);
packet.sampleEntity.update(
sampleEntityRef,
UpdateSampleEntityReq.create()
.setCompare(CompareSampleEntityParam
.create()
.setCode("initial code")
.setName("initial name")
)
);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet))
.isInstanceOf(CompareNotEqualException.class)
.hasMessage("Ошибка обработки команды id = '1', name = 'update': Ошибка обработки сравниваемого поля 'code': " +
"Расхождение ожидаемого 'initial code' и фактического 'code' значений");
В примере выполнение команды update связано с проверкой текущих значений свойств code и name. Значения не соответствуют ожидаемым:
выполнение пакета завершается исключением CompareNotEqualException.
При работе с inc существует возможность определения проверки на соответствие вычисленного значения условию.
Если условие выполняется, то новое значение поля является ошибочным, и будет сформировано исключение уровня пакета IncFailException.
Для определения условия используется класс public class IncrementalFail<T>. Статический конструктор класса:
public static <T> IncrementalFail<T> of(IncrementalFailOperator operator, T value)
Содержит два параметра:
operator: обязательное поле логического оператора сравнения;value: обязательное поле, со значением которого сравнивается вычисленное значение;<T>: соответствует типу параметраInc...Param.
Перечисление операторов логического сравнения:
public enum IncrementalFailOperator {
LESS,
LESS_OR_EQUAL,
GREATER,
GREATER_OR_EQUAL
}
Описание:
LESS— меньше;LESS_OR_EQUAL— меньше или равно;GREATER— больше;GREATER_OR_EQUAL— больше или равно.
Пример объекта для проверки меньше нуля:
IncrementalFail.of(IncrementalFailOperator.LESS, 0);
Пример использования в пакете:
final Packet packet = Packet.createPacket();
final SampleEntityRef sampleEntityRef = packet.sampleEntity.create(
CreateSampleEntityParam
.create()
.setSum(BigDecimal.valueOf(3.14))
);
packet.sampleEntity.update(
sampleEntityRef,
UpdateSampleEntityReq
.create()
.setInc(inc -> inc
.setSum(
BigDecimal.valueOf(5).negate(),
IncrementalFail.of(
IncrementalFailOperator.LESS,
BigDecimal.ZERO
)
)
)
);
assertThatCode(() -> packetClient.execute(packet))
.isInstanceOf(IncFailException.class)
.hasMessageContaining("Ошибка обработки инкриминируемого поля 'sum': Новое значение поля '-1.86', " +
"полученное после сложения с '-5', нарушает ограничение 'LESS 0'");
Описание примера:
первая команда создает экземпляр класса
SampleEntityсо значением3.14в полеsum;вторая команда через метод
incвыполняет изменение значения поляsumна-5, то есть вычисленное значение3.14 - 5 = -1.86. Параметрincсодержит объектfailс условиемменьше нуля(operator == LESS, value == 0), при выполнении которого формируется ошибкаIncFailException.
Команда чтения сущности get#
Команда позволяет получить состояние сущности в момент применения.
Показанное в примере применение команды get похоже на грязное чтение. Пакет выполняется в одной транзакции и при
успешном завершении зафиксируются конечные свойства сущностей. Все команды пакета выполняются последовательно,
поэтому чтение возвращает состояние в моменте своего выполнения. Сигнатуры команды на примере ProductParty:
ProductPartyGet get(ProductPartyRef productParty, Consumer<ProductPartyWithLinkable<ProductPartyGrasp>> param);
ProductPartyGet get(
ProductPartyRef productParty,
Consumer<ProductPartyWithLinkable<ProductPartyGrasp>> param,
Consumer<PacketGetCommandConfiguration<ProductPartyGrasp>> configConsumer
);
Если генерация SDK выполнена с параметром <allowCalculatedWithGetParam>false</allowCalculatedWithGetParam>, сигнатура метода имеет вид:
ProductPartyGet get(ProductPartyRef productParty, Consumer<ProductPartyWith> param);
ProductPartyGet get(
ProductPartyRef productParty,
Consumer<ProductPartyWith> param,
Consumer<PacketGetCommandConfiguration<ProductPartyGrasp>> configConsumer
);
Генерируемые классы ProductPartyWith, ProductPartyWithLinkable, ProductPartyGrasp и ProductPartyGet описывают запрашиваемые свойства и результат после выполнения команды на сервере соответственно.
Команда построена на базе метода search с ограничением поиска одной сущности по идентификатору.
Для отсутствующей сущности команда генерирует исключение ObjectNotFoundException.
Подробнее о заполнении параметра With и составе результата Get см. в разделе «Использование поискового SDK».
Описание параметров:
param: запрашиваемые поля сущности;cond: условие поиска по аналогии с поисковыми запросами (см. раздел «Использование поискового SDK»);configConsumer: дополнительная конфигурация команды:setCommandId— для определения идентификатора команды;setNeedAggregateVersion— определяет необходимость запроса версии агрегата;partCond- позволяет задать лямбду, определяющую условие для ограничения списка секций для секционированных таблиц (см. раздел Ограничение выборки и сортировка).setFailOnEmptyиsetNotFailOnEmpty— определяют реакцию на пустой результат (детали см. в разделе «Ошибка при отсутствующей записи»);lock— пессимистическая блокировка записи уровня БД select for update. Возможные значения:NOT_USE: блокировка не выполняется (умолчание);WAIT: блокировка ожидания освобождения ресурса;NOWAIT: для блокированной другой транзакцией записи пакет будет завершен ошибкойDataAccessExceptionс текстом сообщения зависящим от используемого драйвера БД.
Пример использования команды:
final String productCode = "product code";
final String productName = "product name";
final String serviceCode = "service code";
final String serviceName = "service name";
final Packet packet = Packet.createPacket();
final ProductPartyRef productPartyRef = packet.productParty.create(p -> p
.setCode(productCode)
.setName(productName)
);
final PerformedServiceRef performedServiceRef = packet.performedService.create(p -> p
.setProduct(productPartyRef)
.setCode(serviceCode)
.setName(serviceName)
);
final ProductPartyGet productPartyGet = packet.productParty.get(
productPartyRef,
productPartyWith -> productPartyWith
.withName()
.withCode()
.withPerformedServices(
performedServiceCollectionWith -> performedServiceCollectionWith
.withCode()
.withName()
)
);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet)).doesNotThrowAnyException();
assertThat(productPartyGet.getCode()).isEqualTo(productCode);
assertThat(productPartyGet.getName()).isEqualTo(productName);
final Optional<PerformedServiceGet> serviceGet = productPartyGet.getPerformedServices().stream().findFirst();
assertThat(serviceGet).isPresent();
assertThat(serviceGet.get().getCode()).isEqualTo(serviceCode);
assertThat(serviceGet.get().getName()).isEqualTo(serviceName);
Пример get запроса с установкой условия для ограничения списка секций для секционированных таблиц:
ProductPartyGet res = packet.productParty.get(
productPartyRef,
it -> it.withCode().withPerformedServices(ps -> ps.withCode()),
conf -> conf.setPartCond(partCond -> partCond.id().greaterOrEq("202401_").and(partCond.id().less("202402_"))));
Чтение по условию#
Базовое поведение команды get позволяет выполнить чтение сущности по ее идентификатору, в случае отсутствия сущности формируется ошибка ObjectNotFoundException.
Существует возможность чтения сущности по другим поисковым условиям.
Результат условия должен обеспечивать получение единственной записи или их отсутствие.
Если по условию найдено несколько сущностей, то будет сформирована ошибка TooManyResultsException.
Отсутствие записи не приведет к формированию ошибки, в ответе пакета результат выполнения команды будет представлен пустим объектом.
Для работы с таким вариантом команды get в пакете определены следующие методы (на примере ProductParty):
public ProductPartyGet getByFind(Consumer<ProductPartyWithLinkable<ProductPartyGrasp>> param,
Function<ProductPartyGrasp, ConditionWrapper> cond) { }
public ProductPartyGet getByFind(Consumer<ProductPartyWithLinkable<ProductPartyGrasp>> param,
Function<ProductPartyGrasp, ConditionWrapper> cond,
Consumer<PacketGetCommandConfiguration> configConsumer) { }
Описание параметров:
param: запрашиваемые поля сущности;cond: условие поиска по аналогии с поисковыми запросами (см. раздел «Использование поискового SDK»);configConsumer: дополнительная конфигурация команды:setCommandId— для определения идентификатора команды;setNeedAggregateVersion— определяет необходимость запроса версии агрегата;partCond- позволяет задать лямбду, определяющую условие для ограничения списка секций для секционированных таблиц (см. раздел Ограничение выборки и сортировка).setFailOnEmptyиsetNotFailOnEmpty— определяют реакцию на пустой результат (детали см. в разделе «Ошибка при отсутствующей записи»);lock— пессимистическая блокировка записи уровня БД select for update. Возможные значения:NOT_USE: блокировка не выполняется (умолчание);WAIT: блокировка ожидания освобождения ресурса;NOWAIT: для блокированной другой транзакцией записи пакет будет завершен ошибкойDataAccessExceptionс текстом сообщения зависящим от используемого драйвера БД.
Пример поиска сущности по условию:
final Packet packet = Packet.createPacket();
final ProductPartyGet productPartyGet = packet.productParty.getByFind(
g -> g.withName().withCode(),
w -> w.codeEq(UUID.randomUUID().toString())
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(productPartyGet.$isEmptyResult()).isTrue();
В примере выполняется поиск несуществующей сущности. При обращении к полям результата (productPartyGet) такой команды будет возникать ошибка.
Чтобы проверить, является ли результат выполнения команды пустым, необходимо воспользоваться методом $isEmptyResult().
Следует учитывать, что обращение к этому методу до исполнения пакета приведет к ошибке.
Пример get запроса с установкой условия для ограничения списка секций для секционированных таблиц:
ProductPartyGet res = packet.productParty.getByFind(
it -> it.withCode().withPerformedServices(ps -> ps.withCode()),
where -> where.id().eq(productPartyRef.getId()),
conf -> conf.setPartCond(partCond -> partCond.id().greaterOrEq("202401_").and(partCond.id().less("202402_"))));
Следующий пример показывает некоторые ошибки:
final String code = UUID.randomUUID().toString();
final Packet packet = Packet.createPacket();
final ProductPartyRef productPartyRef = packet.productParty.create(p -> {
});
IntStream
.rangeClosed(1, 2)
.forEach(index -> packet.performedService.create(p -> p
.setProduct(productPartyRef)
.setCode(code)
)
);
final PerformedServiceGet performedServiceGet = packet.performedService.getByFind(
g -> g.withProduct().withCode(),
w -> w.codeEq(code)
);
assertThatCode(performedServiceGet::$isEmptyResult)
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Попытка получения свойства у пустого объекта. " +
"Необходимо прежде выполнить запрос на получение спецификации объекта.");
assertThatCode(() -> packetClient.execute(packet))
.isInstanceOf(TooManyResultsException.class)
.hasMessageContaining("Ошибка обработки команды id = '3', name = 'get': " +
"Too many results");
Связка с другими командами пакета#
Ссылка на команду пакета getByFind может быть использована для формирования идентификатора ...Ref через статический конструктор byGet.
Следующий пример показывает изменение найденной по условию сущности:
final Packet packet = Packet.createPacket();
final ProductPartyGet productPartyGet = packet.productParty.getByFind(
g -> { },
w -> w.codeEq("sample code")
);
packet.productParty.update(
ProductPartyRef.byGet(productPartyGet),
p -> p.setName("sample name")
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
В примере выполняется:
поиск сущности
ProductPartyсо значениемsample codeв полеcode;изменение сущности
ProductParty, где идентификатор сущности строитсяProductPartyRef.byGet(productPartyGet)на основе найденного командойgetByFind.
Метод byGet можно использовать только с командами пакета.
Ошибка при отсутствующей записи#
По умолчанию при чтении сущности для отсутствующей записи:
по идентификатору — формируется ошибка
ObjectNotFoundException;поисковым условием — ошибки не формируется, результат представлен пустым объектом.
Для изменения этого поведения следует использовать методы get или getByFind с параметром Consumer<PacketGetCommandConfiguration>.
На примере команд для ProductParty:
public ProductPartyGet get(ProductPartyRef productParty,
Consumer<ProductPartyWithLinkable<ProductPartyGrasp>> param,
Consumer<PacketGetCommandConfiguration> configConsumer) { }
public ProductPartyGet getByFind(Consumer<ProductPartyWithLinkable<ProductPartyGrasp>> param,
Function<ProductPartyGrasp, ConditionWrapper> cond,
Consumer<PacketGetCommandConfiguration> configConsumer) { }
Экземпляр класса PacketGetCommandConfiguration содержит методы:
setFailOnEmpty()— определяет необходимость формировать ошибкуObjectNotFoundExceptionдля пустого результата команды;setNotFailOnEmpty()— отключает необходимость формировать ошибкуObjectNotFoundExceptionдля пустого результата команды.
Пример изменения поведения по умолчанию для чтения по идентификатору:
final Packet packet = Packet.createPacket();
packet.productParty.get(
ProductPartyRef.of(UUID.randomUUID().toString()),
g -> { },
config -> config.setNotFailOnEmpty()
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
Пример изменения поведения по умолчанию для чтения через поиск:
final Packet packet = Packet.createPacket();
packet.productParty.getByFind(
g -> { },
w -> w.idEq(UUID.randomUUID().toString()),
config -> config.setFailOnEmpty()
);
assertThatCode(() -> packetClient.execute(packet))
.isInstanceOf(ObjectNotFoundException.class);
Внимание!
В пакете с запросом версии агрегата, состоящем только из команд
get, первая команда должна гарантировать получение записи, то есть выполнять чтение по идентификатору без использования строкового условия. ПолеfailOnEmptyдолжно отсутствовать или иметь значениеtrue.
Команда удаления сущности delete#
Команда позволяет удалить сущность по ее идентификатору. Для отсутствующей сущности команда генерирует исключение ObjectNotFoundException.
Сигнатура команды на примере ProductParty:
void delete(ProductPartyRef productParty);
void delete(ProductPartyRef productParty, CompareProductPartyParam compare);
Для типа сформированы две команды, т.к. структура типа допускает применения compare (описание в разделе «Команда изменения сущности update»).
Пример использование команды:
final Packet packet = Packet.createPacket();
final ProductPartyRef productPartyRef = packet.productParty.create(CreateProductPartyParam.create());
packet.productParty.delete(productPartyRef);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet)).doesNotThrowAnyException();
В примере выполняется создание и удаление сущности, что приведено в демонстрационных целях, т.к. практического смысла не имеет.
Следующий пример показывает использование compare:
final Packet packet = Packet.createPacket();
// создание продукта со значением `code` равным `product code`
final ProductPartyRef productPartyRef = packet.productParty.create(
CreateProductPartyParam
.create()
.setCode("product code")
);
// изменение значения `code` на новое значение `new product code`
packet.productParty.update(
productPartyRef,
UpdateProductPartyParam
.create()
.setCode("new product code")
);
// удаление продукта с проверкой ожидаемого значения свойства `code` равным `product code`
packet.productParty.delete(
productPartyRef,
CompareProductPartyParam
.create()
.setCode("product code")
);
// выполнение пакета завершено с ошибкой
assertThatCode(() -> dataspaceCorePacketClient().execute(packet))
.isInstanceOf(CompareNotEqualException.class)
.hasMessage("Ошибка обработки команды id = '2', name = 'delete': Ошибка обработки сравниваемого поля 'code':" +
" Расхождение ожидаемого 'product code' и фактического 'new product code' значений");
Описание работы команды delete#
При удалении сущности выполняется каскадное удаление сущностей, связанных с удаляемой свойством с признаком parent="true".
Прочие ссылки на удаляемые сущности никак не проверяются и не обрабатываются, что может привести к появлению «битых» ссылок. В схеме базы данных отсутствуют ограничения по внешним ключам, что может привести к появлению записей со ссылками на удаленные родительские сущности при параллельном выполнении транзакций вставки и удаления элементов дерева родителя. Следует учитывать такое поведение в реализации прикладной логики, использовать оптимистические/прикладные блокировки при работе с конкуренцией в данных.
Модуль dataspace-core содержит параметр dataspace.useAggregateParentStrongConstraintCheck, управляющий логикой блокировки корневых сущностей в командах создания и изменения, а также всего дерева сущностей при удалении.
По умолчанию параметр имеет значение «false», то есть блокировка выключена.
При включении блокировки следует учитывать повышенное влияние на производительность и потребляемые ресурсы.
Работа с embedded-классами в командах пакета#
Правила определения embedded-классов в модели см. в разделе «Embedded-классы» документа «Руководство по ведению модели данных»
Пример использования:
Packet packet = Packet.createPacket();
// создание и наполнение структуры для создания embedded-атрибутов
AddressCreateObject addressCreateObject
= new AddressCreateObject()
.setCity("Saint-Petersburg")
.setStreet("Rubinshtein st.")
.setHouseNumber("24");
// формирование команды на создание экземпляра
BookStoreRef sampleRef = packet.bookStore.create(
e -> {
// установка значений полей для embedded-объекта напрямую
e.setAddressReg(af -> af
.setCity("Moscow")
.setStreet("Nikolskaya st.")
.setHouseNumber("42")
);
// установка значений полей для embedded-объекта через структуру [имя класса]CreateObject
e.setAddressFact(oe -> oe.copyFieldsFrom(addressCreateObject));
});
//запрос результата
BookStoreGet bookStoreGet
= packet.bookStore.get(sampleRef, e -> {
e.withAddressFact(re -> {
re.withCity().withStreet().withHouseNumber();
}).withAddressReg(oe -> {
oe.withCity().withStreet();
});
});
dataspaceCorePacketClient.execute(packet);
Assert.assertTrue("Moscow".equals(bookStoreGet.getAddressReg().getCity()));
Assert.assertTrue("Saint-Petersburg".equals(bookStoreGet.getAddressFact().getCity()));
Packet secondPacket = Packet.createPacket();
//формирование команды на изменение экземпляра BookStore
secondPacket.bookStore.update(sampleRef, e -> {
// установка null-значений для всех полей структуры
e.setAddressFactNull();
});
//запрос результата
BookStoreGet bookStoreGetAfterUpdate
= secondPacket.bookStore.get(sampleRef, e -> {
e.withAddressFact(re -> {
re.withCity().withStreet().withHouseNumber();
}).withAddressReg(oe -> {
oe.withCity().withStreet();
});
});
dataspaceCorePacketClient.execute(secondPacket);
Assert.assertTrue(null == bookStoreGetAfterUpdate.getAddressFact().getCity());
Команды прикладной блокировки сущности tryLock и unlock#
Команды доступны для типов модели с указанием атрибута класса lockable="true".
Сигнатуры методов на примере ProductParty:
LockRs tryLock(ProductPartyRef productParty, Consumer<LockRq> param);
LockRs unlock(ProductPartyRef productParty, String appLockToken);
Подробно о работе команды, структуре классов LockRs и LockRq описано в разделе «Использование прикладных блокировок ресурсов».
Команды Many#
Команды пакета ориентированы на работы с единственным экземпляром сущности.
Для выполнения множественных действий команды create, update, updateOrCreate и delete имеют аналоги Many, принимающие список параметров.
Команда создания сущностей по списку createMany#
Сигнатура команды на примере `ProductParty``:
CreateManyRef createMany(List<CreateProductPartyParam> items) {}
CreateManyRef createMany(List<CreateProductPartyParam> items, String id) {}
Описание:
items: список параметров создаваемых сущностей, тип списка аналогичен командеcreate. Список должен содержать значения;id: опциональный идентификатор команды в пакете;CreateManyRef: ссылка на результат команды, включает методы:List<String> getIds(): список идентификаторов созданных сущностей в порядке элементов спискаitems;String commandElementPathByIndex(int index): метод получения ссылки на идентификатор сущности по индексу;boolean isEmptyResult(): признак выполнения команды при наличии зависимости от другой.
Пример создания двух сущностей с последующим чтением:
var packet = Packet.createPacket();
var createMany = packet.productParty.createMany(
List.of(
CreateProductPartyParam.create().setId("1").setCode("1"),
CreateProductPartyParam.create().setId("2").setCode("2")
)
);
var getFirst = packet.productParty.get(
ProductPartyRef.of(createMany.commandElementPathByIndex(0)),
ProductPartyWithLinkable::withCode
);
var getSecond = packet.productParty.get(
ProductPartyRef.of(createMany.commandElementPathByIndex(1)),
ProductPartyWithLinkable::withCode
);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet)).doesNotThrowAnyException();
assertThat(createMany.getIds()).hasSize(2);
assertThat(createMany.getIds()).containsExactly("1", "2");
assertThat(getFirst.getCode()).isEqualTo("1");
assertThat(getSecond.getCode()).isEqualTo("2");
В примере показано использование конструкции ProductPartyRef.of(createMany.commandElementPathByIndex(0)) для получения
ссылки на создаваемую сущность в командах пакета.
Команда изменения сущностей по списку updateMany#
Сигнатура команды на примере `ProductParty``:
PacketCommandRef updateMany(List<UpdateManyItem<ProductPartyRef, UpdateProductPartyParam>> items) {}
PacketCommandRef updateMany(List<UpdateManyItem<ProductPartyRef, UpdateProductPartyParam>> items, String id) {}
Описание:
items: список параметров команды для изменяемых сущностей. Список должен содержать значения;id: опциональный идентификатор команды в пакете;PacketCommandRef: ссылка на результат команды, включает метод:boolean isEmptyResult(): признак выполнения команды при наличии зависимости от другой;
UpdateManyItem: параметр команды для изменяемой сущности предназначен для определения ссылки на сущность, и изменяемые значения по аналогии с командойupdate.
Пример изменения двух сущностей:
packet.productParty.updateMany(
List.of(
UpdateManyItem.of(
ProductPartyRef.of("1"),
UpdateProductPartyParam.create().setCode("1-updated")
),
UpdateManyItem.of(
ProductPartyRef.of("2"),
UpdateProductPartyParam.create().setCode("2-updated")
)
)
);
Для команды updateMany с указанной сигнатурой выполняется чтение сущностей по идентификаторам запросами в пределах
лимита параметра dataspace-core.updateManyReadInstancesLimit со значением по умолчанию 999.
Такое чтение должно исключать отдельные чтения по каждой сущности в команде.
Следует понимать, что параметр определяет количество элементов оператора in SQL запроса, и оптимальное количество необходимо определить через тестирование.
Значение параметра 0 отключает функциональность.
Если класс сущности содержит поля для поддержки Inc/Compare, то становятся доступными дополнительные методы:
PacketCommandRef updateMany(UpdateManyList<ProductPartyRef, UpdateProductPartyReq> items) {}
PacketCommandRef updateMany(UpdateManyList<ProductPartyRef, UpdateProductPartyReq> items, String id) {}
Описание:
UpdateManyList<ProductPartyRef, UpdateProductPartyReq>: определяет список параметров команды изменяемых сущностей с дополнительными методамиIncиCompare. Включает статические конструкторы для определения списка:<R, C> UpdateManyList<R, C> of(UpdateManyItem<R, C>... items);<R, C> UpdateManyList<R, C> by(List<UpdateManyItem<R, C>> items).
Пример:
packet.productParty.updateMany(
UpdateManyList.of(
UpdateManyItem.of(
ProductPartyRef.of(createMany.commandElementPathByIndex(0)),
UpdateProductPartyReq.create()
.setParam(p -> p.setCode("1-1"))
.setInc(inc -> inc.setCounter(1))
.setCompare(c -> c.setCode("1"))
),
UpdateManyItem.of(
ProductPartyRef.of(createMany.commandElementPathByIndex(1)),
UpdateProductPartyReq.create()
.setParam(p -> p.setCode("2-1"))
.setInc(inc -> inc.setCounter(1))
.setCompare(c -> c.setCode("2"))
)
)
);
В примере используется ссылка на команду из описания createMany.
Команда изменения или создания сущностей по списку updateOrCreateMany#
Сигнатура команды на примере `ProductParty``:
UpdateOrCreateManyRef updateOrCreateMany(List<UpdateOrCreateManyItem<CreateProductPartyParam, ExistProductPartyParam>> items) {}
UpdateOrCreateManyRef updateOrCreateMany(List<UpdateOrCreateManyItem<CreateProductPartyParam, ExistProductPartyParam>> items, String id) {}
Описание:
items: список параметров команды для изменяемых или создаваемых сущностей. Список должен содержать значения;id: опциональный идентификатор команды в пакете;UpdateOrCreateManyRef: ссылка на результат команды, включает методы:List<UpdateOrCreateManyResult> getResults(): список результатов выполнения по аналогии с командойupdateOrCreate. КлассUpdateOrCreateManyResultвключает методы:boolean isCreated(): признак создания сущности командой;String getId(): идентификатор сущности;
String commandElementPathByIndex(int index): метод получения ссылки на идентификатор сущности по индексу;boolean isEmptyResult(): признак выполнения команды при наличии зависимости от другой;
UpdateOrCreateManyItem: комбинация параметров создания и изменения сущности по аналогии с командойupdateOrCreate.
Пример использования команды в связке с командой createMany:
var updateOrCreateMany = packet.productParty.updateOrCreateMany(
List.of(
UpdateOrCreateManyItem.of(
CreateProductPartyParam.create().setId(createMany.commandElementPathByIndex(0)),
ExistProductPartyParam.create()
.setUpdate(p -> p.setCode("1-2"))
.setInc(inc -> inc.setCounter(1))
.setCompare(c -> c
.setCode("1-1")
.setCounter(1)
)
),
UpdateOrCreateManyItem.of(
CreateProductPartyParam.create().setId(createMany.commandElementPathByIndex(1)),
ExistProductPartyParam.create()
.setUpdate(p -> p.setCode("2-2"))
.setInc(inc -> inc.setCounter(1))
.setCompare(c -> c
.setCode("2-1")
.setCounter(1)
)
),
UpdateOrCreateManyItem.of(
CreateProductPartyParam.create().setId("3").setCounter(1).setCode("3"),
ExistProductPartyParam.create()
)
)
);
// прочие методы и исполнение пакета
assertThat(updateOrCreateMany.getResults()).hasSize(3);
assertThat(updateOrCreateMany.getResults().get(0).isCreated()).isFalse();
assertThat(updateOrCreateMany.getResults().get(0).getId()).isEqualTo("1");
assertThat(updateOrCreateMany.getResults().get(1).isCreated()).isFalse();
assertThat(updateOrCreateMany.getResults().get(1).getId()).isEqualTo("2");
assertThat(updateOrCreateMany.getResults().get(2).isCreated()).isTrue();
assertThat(updateOrCreateMany.getResults().get(2).getId()).isEqualTo("3");
Команда удаления сущностей по списку deleteMany#
Сигнатура команды на примере `ProductParty``:
PacketCommandRef deleteMany(List<ProductPartyRef> items) {}
PacketCommandRef deleteMany(List<ProductPartyRef> items, String id) {}
Описание:
items: список ссылок удаляемых сущностей. Список должен содержать значения;id: опциональный идентификатор команды в пакете;PacketCommandRef: ссылка на результат команды, включает метод:boolean isEmptyResult(): признак выполнения команды при наличии зависимости от другой;
Пример удаления двух сущностей:
packet.productParty.deleteMany(
List.of(
ProductPartyRef.of("1"),
ProductPartyRef.of("2")
));
Если тип сущности поддерживает Compare, то становятся доступными дополнительные методы:
PacketCommandRef deleteMany(DeleteManyList<ProductPartyRef, CompareProductPartyParam> items) {}
PacketCommandRef deleteMany(DeleteManyList<ProductPartyRef, CompareProductPartyParam> items, String id) {}
Описание:
DeleteManyList<ProductPartyRef, CompareProductPartyParam>: определяет список параметров команды удаления сущностей с дополнительным параметромCompare. Включает статические конструкторы для определения списка:<R, C> DeleteManyList<R, C> of(DeleteManyItem<R, C>... items);<R, C> DeleteManyList<R, C> by(List<DeleteManyItem<R, C>> items).
Пример использования команды в связке с updateOrCreateMany:
packet.productParty.deleteMany(
DeleteManyList.of(
DeleteManyItem.of(
ProductPartyRef.of(updateOrCreateMany.commandElementPathByIndex(0)),
CompareProductPartyParam.create().setCounter(2).setCode("1-2")
),
DeleteManyItem.of(
ProductPartyRef.of(updateOrCreateMany.commandElementPathByIndex(1)),
CompareProductPartyParam.create().setCounter(2).setCode("2-2")
),
DeleteManyItem.of(
ProductPartyRef.of(updateOrCreateMany.commandElementPathByIndex(2)),
CompareProductPartyParam.create().setCounter(1).setCode("3")
)
)
);
Примечание#
Команды Many добавляют возможность описания в пакете однотипных команд, но фактически являются их полными аналогами по функциональности и потребляемым ресурсам.
Обновление/удаление данных по условию (updateFor/deleteFor)#
Команды реализуют возможность выполнения update/delete по определенному условию.
Общие сведения#
Вначале производится выборка записей, удовлетворяющих условию отбора, после чего по выборке производится изменение/удаление индивидуальных записей.
Поскольку объем обрабатываемых данных потенциально может приводить к исчерпанию ресурсов сервиса dataspace-core или превышению ограничений интеграций, предусмотрен лимит на количество обрабатываемых записей.
Лимит системного ограничения определяется настройкой dataspace-core.commandFor.defaultLimit сервиса dataspace-core, и имеет значение по умолчанию 10000.
Внимание!
Установленный по умолчанию лимит имеет очень высокое ограничение. Фактическое значение лимита следует устанавливать после проведения НТ.
При нулевом значении настройки системное ограничение снимается. При обработке команды лимит устанавливается в заданное настройкой значение (если > 0) или в значение, указанной в параметре команды (если значение в параметре команды меньше значения настройки). Для выполнения команды обязательны к указанию:
критерий отбора;
ограничение на количество обрабатываемых командой записей (limit).
Результатом выполнения команды является количество обработанных записей (count) и использованный лимит (limit). В качестве limit возвращается минимум из двух значений:
значение параметра команды limit;
системный лимит, заданный настройкой (системный лимит, равный 0, не учитывается).
Признаком того, что обработаны все записи, является значение count < limit (при условии, что за время выполнения пакета не появилось новых записей, удовлетворяющих критерию отбора).
При выполнении изменения/удаления индивидуальной записи условие проверяется повторно, что гарантирует изменение/удаление только удовлетворяющих условию записей.
Все изменения происходят в одной транзакции БД (в случае изменения сущностей разных агрегатов должна быть явно разрешена мультиагрегатная транзакция).
Не гарантируется, что за время выполнения операции не появятся новые записи, удовлетворяющие условию, в том числе те, которые в соответствии с критерием сортировке должны быть в начале выборки.
Использование команд в идемпотентном пакете имеет следующие особенности:
deleteFor,updateForдолжна быть не первой операцией изменения данных в пакете;если операция
deleteFor,updateForпервая, то условие должно вернуть хотя бы одну запись (выполнение как минимум одной операции update/delete).
Доступность команд для типов сущностей#
По умолчанию команды updateFor и deleteFor исключены из списка доступных. Для использования команд необходимо выполнить следующее:
добавить в конфигурацию плагина
model-api-generator-maven-pluginнастройкуcommandForEntities, где через запятую перечислить имена типов сущностей, для которых доступны команды. Настройку следует добавить для целейcreateModelиcreateSdk;собранные артефакты модели использовать в сервисе
dataspace-core. Важно, без запуска модуля с собранной моделью, команды нельзя использовать.
При генерации модели плагином model-api-generator-maven-plugin выполняется проверка корректности указанных наименований типов сущности в модели.
Если тип сущности не найден, то генерируется ошибка вида Параметр 'commandForEntities' содержит неопределенные в модели сущности: FakeClass, где FakeClass один из некорректно указанных типов.
Если тип сущности исключен из настройки для ранее выпущенной модели, то при генерации модели формируется сообщение
вида WARNING: В новом значении параметра 'commandForEntities' исключены сущности: SampleEntity, где SampleEntity один из исключенных типов.
Пример настройки:
<plugin>
<groupId>sbp.com.sbt.dataspace</groupId>
<artifactId>model-api-generator-maven-plugin</artifactId>
<executions>
<execution>
<id>createModel</id>
<goals>
<goal>createModel</goal>
</goals>
<configuration>
<!-- прочие настройки плагина -->
<commandForEntities>ProductParty,PerformedService</commandForEntities>
</configuration>
</execution>
</executions>
</plugin>
Сервис dataspace-core имеет дополнительную настройку dataspace-core.commandFor.extraEntities, в которой перечисляются дополнительные к списку из параметра commandForEntities имена типов сущностей для активации команд в протоколах json-rpc и GraphQL.
Данная настройка не оказывает влияние на генерируемые API SDK.
Сигнатура методов#
Для типов сущностей, которым доступны команды updateFor и deleteFor, в API SDK генерируются следующие методы (на примере ProductParty):
CommandForRef updateFor(Consumer<UpdateForProductPartyParam> updateForProductPartyParam) { }
CommandForRef updateFor(Consumer<UpdateForProductPartyParam> updateForProductPartyParam, String id) { }
CommandForRef deleteFor(Consumer<DeleteForProductPartyParam> deleteForProductPartyParam) { }
CommandForRef deleteFor(Consumer<DeleteForProductPartyParam> deleteForProductPartyParam, String id) { }
Описание:
CommandForRef— ссылка на результат выполнения команды. Содержит методы:int getCount()— количество обработанных записей;int getLimit()— лимит выборки;boolean isEmptyResult()— возвращаетtrue, если команда содержит пустой результат;
UpdateForProductPartyParam— зависимый от типа сущности класс параметров командыupdateFor. Содержит:filter(Consumer<CommandForFilter<ProductPartyGrasp>> filter)— параметры фильтрации:CommandForFilter<G> cond(Function<G, ConditionWrapper> condition)— условия отбора записей;CommandForFilter<G> sorting(Consumer<AdvancedSortBuilder<G>> sorting)— сортировка выборки;CommandForFilter<G> limit(int limit)— лимит;
UpdateForProductPartyParam update(Consumer<UpdateProductPartyParam> updateConsumer)— параметр обновления сущности, аналогичен командеupdate;UpdateForProductPartyParam inc(Consumer<IncProductPartyParam> incConsumer)— параметрIncпо аналогии с командойupdate;UpdateForProductPartyParam compare(Consumer<CompareProductPartyParam> compareConsumer)— параметрCompareпо аналогии с командойupdate;
id- идентификатор команды в пакете;DeleteForProductPartyParam— зависимый от типа сущности класс параметров командыdeleteFor. Содержит:filter- аналогичен командеupdateFor;compare- аналогичен командеupdateFor.
Важно: параметры cond и limit обязательны для заполнения.
Пример использования:
final var code = uuid();
final var size = 3;
final var limit = 10;
{
var packet = Packet.createPacket();
IntStream.rangeClosed(1, size).forEach(index -> packet.productParty.create(p -> p
.setCode(code + "-" + index))
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
}
var packet = Packet.createPacket();
var updateFor = packet.productParty.updateFor(p -> p
.filter(f -> f
.cond(cond -> cond.code().like(code + "-%"))
.limit(limit)
)
.update(u -> u.setCode(code + "-updated"))
);
var deleteFor = packet.productParty.deleteFor(p -> p
.filter(f -> f
.cond(cond -> cond.code().like(code + "-up%"))
.limit(limit)
)
.compare(c -> c.setCode(code + "-updated"))
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(updateFor.getCount()).isEqualTo(size);
assertThat(updateFor.getLimit()).isEqualTo(limit);
assertThat(deleteFor.getCount()).isEqualTo(size);
assertThat(deleteFor.getLimit()).isEqualTo(limit);
Пояснение к примеру:
первый пакет создает три сущности типа
ProductPartyсо значением поляcodeсоответствующим маске...-X, где X значение от 1 до 3;второй пакет:
выполняет команду
updateForс фильтром по полюcode, где значение похоже на созданные ранее сущности, и для каждой сущности выполняется изменение поляcodeна фиксированное значение...-updated;выполняет команду
deleteForс фильтром по полюcode, где значение похоже на ранее измененное, и выполняет удаление каждой отобранной сущности с проверкой, что полеcodeимеет значение...-updated;
Условное выполнение команд#
Команды пакета выполняются последовательно. Если выполняемая команда завершается ошибкой, то выполнение пакета прерывается и формируется ошибочный результат, иначе результат пакета будет содержать результат выполнения входящих в него команд.
Существует возможность определить необходимость выполнения команды в пакете на основании результата выполнения другой команды (или других команд).
В качестве источника результата выступают команды getи updateOrCreate.
Для определения зависимости выполнения команды от результата выполнения других команд в классе Packet определен метод:
public <T extends PacketCommandRef> T dependsOn(T packetCommandRef, Consumer<CommandDependsOn> dependsOnConsumer)
Параметры которого:
packetCommandRef: ссылка на команду для которой определяется зависимость. Допускаются любые команды пакета кромеget;dependsOnConsumer: определяет условия зависимости относительно результата выполнения других команд.
Класс условий выполнения команды (основные методы):
public class CommandDependsOn {
public CommandDependsOn addGetDependency(AbstractProxyGet abstractProxyGet, GetDependency getDependency) { }
public CommandDependsOn addUpdateOrCreateDependency(PacketEntity ref, UpdateOrCreateDependency updateOrCreateDependency) { }
public CommandDependsOn addUpdateOrCreateDependency(UpdateOrCreateManyRef ref, int index, UpdateOrCreateDependency updateOrCreateDependency) { }
public CommandDependsOn addByFieldDependency(AbstractProxyGet abstractProxyGet, String fieldName) { }
public CommandDependsOn addCommandForDependency(CommandForRef commandForRef, CommandForDependency commandForDependency) { }
}
Описание:
addGetDependency: зависимость относительно командыget. Параметры метода:abstractProxyGet: ссылка нa командуget, результат которой анализируется для условия выполнения;getDependency: перечисление с вариантами анализа результата:EXISTS— командаgetимеет не пустой результат;NOT_EXISTS— командаgetимеет пустой результат;
addUpdateOrCreateDependency: зависимость относительно командыupdateOrCreate. Параметры метода:ref: ссылка на командуupdateOrCreate/updateOrCreateMany, результат которой анализируется для условия выполнения;updateOrCreateDependency: перечисление с вариантами анализа результата:CREATED— командаupdateOrCreateимеет значениеcreated, равнымtrue, то есть в результате ее выполнения была создана сущность;NOT_CREATED— командаupdateOrCreateимеет значениеcreated, равнымfalse, то есть сущность была создана ранее;
index: индекс элемента командыupdateOrCreateMany;
addByFieldDependency: зависимость относительно значения одного из полей командыget. Параметры метода:abstractProxyGet: ссылка нa командуget, результат которой анализируется для условия выполнения;fieldName: поле, значение которого определяет выполнение (значениеtrue), или не выполнение (значениеfalse) зависимой команды. Особенности применения:тип значения поля должен быть
Boolean;если поле содержит
null, то формируется ошибка...результата команды не содержит значения true/false;если команда
getимеет пустой результат, то общее условие будетfalse;если поле не является вложенным относительно корня результата команды
get, то достаточно указать просто имя, иначе следует использовать описание полного пути/props/...
addCommandForDependency: зависимость относительно результата выполнения командupdateFor/deleteFor:commandForRef: ссылка на командуupdateFor/deleteFor, результат которой анализируется для условия выполненияcommandForDependency: перечисление с вариантами анализа результата:COMMAND_FOR_EMPTY: команда имеет пустой результат выбора сущностей для обработки (getCount()равен 0);COMMAND_FOR_NOT_EMPTY: команда имеет не пустой результат выбора сущностей для обработки (getCount()больше 0);COMMAND_FOR_DONE: командой обработаны все записи непустой выборки с размером меньше лимита (getCount()> 0 иgetCount()<getLimit());COMMAND_FOR_CAN_HAVE_MORE: командой обработаны все записи непустой выборки с размером равным лимиту (getCount()==getLimit()), что означает возможное наличие сущностей, удовлетворяющих условию фильтрации.
Объект класса CommandDependsOn формирует массив зависимостей dependsOn, последовательность элементов соответствует порядку вызова методов.
Пример заполнения зависимостями от двух команд:
final PerformedServiceRef performedServiceRef = packet.dependsOn(
packet.performedService.create(
CreatePerformedServiceParam
.create()
.setProduct(productPartyRef)
.setCode(performedServiceCode)
),
commandDependsOn -> commandDependsOn
.addUpdateOrCreateDependency(productPartyRef, UpdateOrCreateDependency.CREATED)
.addGetDependency(performedServiceByFind, GetDependency.NOT_EXISTS)
);
В примере команда create будет выполнена, если результат ранее определенных в пакете команд:
productPartyRef— командаupdateOrCreateсоздаст экземплярProductParty;performedServiceByFind— командаgetсодержит пустой результат, т.е. экземпляр по указанным в этой команде условиям не найден.
Команда выполняется при наличии всех указанных в dependsOn условий, соответствующих true. Анализ условий в массиве выполняется до первого результата false.
Команда из массива dependsOn должна следовать ранее по потоку выполнения команд пакета, то есть должна быть исполнена на момент проверки. Для команды updateOrCreate не допустим пустой результат.
Метод dependsOn не применим к команде get.
Для определения факта выполнения команды можно воспользоваться методом isEmptyResult() ссылки на команду.
Для пропущенной команды метод вернет значение true. Обращение к методу допустимо только после исполнения пакета.
Пример использования:
final String productPartyId = "42";
final String performedServiceCode = "42";
IntStream.rangeClosed(1, 2).forEach(useCase -> {
final Packet packet = Packet.createPacket();
final ProductPartyRef productPartyRef = packet.productParty.updateOrCreate(
CreateProductPartyParam
.create()
.setId(productPartyId)
);
final PerformedServiceGet performedServiceByFind = packet.performedService.getByFind(
g -> {
},
w -> w.codeEq(performedServiceCode)
);
final PerformedServiceRef performedServiceRef = packet.dependsOn(
packet.performedService.create(
CreatePerformedServiceParam
.create()
.setProduct(productPartyRef)
.setCode(performedServiceCode)
),
commandDependsOn -> commandDependsOn
.addUpdateOrCreateDependency(productPartyRef, UpdateOrCreateDependency.CREATED)
.addGetDependency(performedServiceByFind, GetDependency.NOT_EXISTS)
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(performedServiceRef.isEmptyResult()).isEqualTo(useCase == 2);
});
Пояснение к примеру:
на момент выполнения должен отсутствовать экземпляр
ProductPartyс идентификатором42;на момент выполнения должен отсутствовать экземпляр
PerformedServiceсо значением42в полеcode;первая команда пакета
updateOrCreateсоздаст экземплярProductPartyс идентификатором42приuseCaseравным1;вторая команда пакета
getвыполняет поиск экземпляраPerformedServiceсо значением42в полеcode. ДляuseCaseравным1экземпляр отсутствует;третья команда пакета
createсоздает экземплярPerformedServiceсо значением42в полеcodeпри выполнении условий:updateOrCreateдляuseCaseравным1является истиной, т.к. экземпляр создан;getдляuseCaseравным1является истиной, т.к. экземпляр отсутствует;
результат третьей команды
performedServiceRefне пустой дляuseCaseравным1, что проверяется вызовом методаperformedServiceRef.isEmptyResult().
Внимание!
Исполнение читающих команд не приводит к созданию записи идемпотентности. Запись создается только для пишущих команд, а конкретно — для первой пишущей команды. Исходя из этого, первая пишущая команда должна быть безусловной, чтобы запись идемпотентности в любом случае была проверена и при необходимости создана.
Пример использование addByFieldDependency:
@Test
void dependOnByFieldTest() {
final String code = uuid();
final Packet packet = Packet.createPacket();
final ProductPartyRef productPartyRef = packet.productParty.create(p -> p
.setCode(code)
.setNonCCI(true)
.setProductPartySelected(s -> s
.setReason(uuid())
.setSelected(true)
)
);
final ProductPartyGet productPartyGet = packet.productParty.get(
productPartyRef,
g -> g
.withNonCCI()
.withProductPartySelected(ProductPartySelectedWithLinkable::withSelected)
.$withCalculated(
"is-code",
productPartyGrasp -> productPartyGrasp.codeEq(code).asBooleanField()
)
);
final PerformedServiceRef performedServiceRefByNonCCI = packet.dependsOn(
packet.performedService.create(p -> p.setProduct(productPartyRef)),
commandDependsOn -> commandDependsOn.addByFieldDependency(
productPartyGet,
"nonCCI"
)
);
final PerformedServiceRef performedServiceRefByIsCode = packet.dependsOn(
packet.performedService.create(p -> p.setProduct(productPartyRef)),
commandDependsOn -> commandDependsOn.addByFieldDependency(
productPartyGet,
"is-code"
)
);
final PerformedServiceRef performedServiceRefBySelected = packet.dependsOn(
packet.performedService.create(p -> p.setProduct(productPartyRef)),
commandDependsOn -> commandDependsOn.addByFieldDependency(
productPartyGet,
"/props/productPartySelected/selected"
)
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(performedServiceRefByNonCCI.isEmptyResult()).isFalse();
assertThat(performedServiceRefByIsCode.isEmptyResult()).isFalse();
assertThat(performedServiceRefBySelected.isEmptyResult()).isFalse();
}
В примере показаны три варианта использования addByFieldDependency:
на основе значения поля
nonCCI;на основе вычисляемого свойств запроса
is-code(необходимо учитывать, что тип значения должен бытьBoolean);на основе значения поля вложенного объекта
/props/productPartySelected/selected.
Связывание параметров модифицирующих команд с результатом get#
Имеется возможность заполнения параметров модифицирующих команд значениями из результатов выполнения команд get на этапе исполнения пакета.
Для простой демонстрации используется тип сущности:
<class name="SampleEntity">
<property name="code" type="String"/>
<property name="name" type="String"/>
</class>
Пример кода с использованием SDK:
@Test
void simpleBindTest() {
final Packet packet = Packet.createPacket();
final SampleEntityRef sampleEntityRef = packet.sampleEntity.create(p -> p.setCode(UUID.randomUUID().toString()));
final SampleEntityGet sampleEntityGet = packet.sampleEntity.get(sampleEntityRef, SampleEntityWith::withCode);
packet.sampleEntity.update(
sampleEntityRef,
p -> p.bind("name", sampleEntityGet, "/props/code")
);
final SampleEntityGet sampleEntityGetAfterUpdate = packet.sampleEntity.get(
sampleEntityRef,
g -> g
.withCode()
.withName()
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(sampleEntityGetAfterUpdate.getCode()).isEqualTo(sampleEntityGet.getCode());
assertThat(sampleEntityGetAfterUpdate.getName()).isEqualTo(sampleEntityGet.getCode());
}
Пояснение к примеру:
Создается экземпляр сущности с заполнением свойства
code.Для созданного экземпляра выполняется чтение свойства
code.В команде
updateметодbindопределяет необходимость заполнения свойстваnameзначением свойстваcode, полученным в результате выполнения командыget.Последняя команда пакета выполняет чтение свойств
codeиnameдля подтверждения корректности работы.
Пакет в формате JSON-RPC имеет вид:
{
"commands" : [ {
"id" : "0",
"name" : "create",
"params" : {
"type" : "SampleEntity",
"code" : "2f21e721-d24c-43e0-b38d-85cb2fb9d9f4"
}
}, {
"id" : "1",
"name" : "get",
"params" : {
"type" : "SampleEntity",
"props" : "code",
"id" : "ref:0"
}
}, {
"id" : "2",
"name" : "update",
"params" : {
"type" : "SampleEntity",
"id" : "ref:0",
"name" : "ref:1/props/code"
}
}, {
"id" : "3",
"name" : "get",
"params" : {
"type" : "SampleEntity",
"props" : [ "code", "name" ],
"id" : "ref:0"
}
} ]
}
Команда с "id" : "2" в свойстве name содержит значение ref:1/props/code, которое определяет:
ref:1— ссылка на результат команды для использования;/props/code— json-путь к значению свойства в JSON-RPC-формате ответа команды чтения.
В примере команда packet.sampleEntity.get(sampleEntityRef, SampleEntityWith::withCode) формирует ответ:
{
"type" : "SampleEntity",
"id" : "7223314278188253185",
"props" : {
"code" : "2f21e721-d24c-43e0-b38d-85cb2fb9d9f4"
}
}
Из структуры ответа следует, что значение для свойства code может быть получено по пути /props/code.
Примечание
Структура ответа команды чтения зависит от структуры запроса, поэтому для связывания необходимо знать формируемый ответ в формате JSON-RPC.
Если ссылка будет вести на несуществующее поле, то пакет завершится ошибкой.
Сигнатура метода bind включает параметры:
String propertyName— имя свойства команды, для которого выполняется связывание;AbstractProxyGet commandGet— ссылка на командуget, из которой будет браться значение;String path— json-путь к значению. Значение должно начинаться с/. Элементы пути, включая индекс позиции элемента в массиве (первый элемент имеет индекс 0), должны разделяться/. Пример пути к значению поляstreetпервого элемента массивы „addresses“ в json{ "addresses": [ { "street": "Main" } ] }имеет вид `/addresses/0/street``.
Для связывания допустимы свойства примитивных типов, вложенные объекты и внешние ссылки.
При связывании составных объектов необходимо убедиться, что структура json в ответе команды get соответствует структуре свойства команды в формате JSON-RPC.
Ответ выполнения пакета в формате JSON можно получить методом getResponse() объекта сервисного класса BaseDataspacePacketClient.TransportData, возвращаемого методом execute клиента пакета. Пример: String response = packetClient.execute(packet).getResponse().
Следующий пример демонстрирует реализацию счетчика. Модель для примера:
<class name="Counter">
<id category="MANUAL"/>
<property name="current" type="int"/>
</class>
<class name="Sample">
<id category="AUTO_ON_EMPTY"/>
<property name="name" type="String"/>
<property name="index" type="int"/>
</class>
Для работы примера необходимо расширить транзакционные границы пакета:
@Test
void counterTest() {
final String sampleCounterName = "SAMPLE_COUNTER";
IntStream.rangeClosed(1, 3).forEach(value -> {
final Packet packet = Packet.createPacket();
final CounterRef counterRef = packet.counter.updateOrCreate(
CreateCounterParam
.create()
.setId(sampleCounterName)
.setCurrent(1),
ExistCounterParam
.create()
.setUpdate(UpdateCounterParam.create())
.setInc(inc -> inc.setCurrent(1))
);
final CounterGet counterGet = packet.counter.get(counterRef, CounterWith::withCurrent);
final SampleRef sampleRef = packet.sample.create(p -> p
.bind("index", counterGet, "/props/current")
);
final SampleGet sampleGet = packet.sample.get(
sampleRef,
SampleWith::withIndex
);
assertThatCode(() -> packetClient.execute(packet)).doesNotThrowAnyException();
assertThat(sampleGet.getIndex()).isEqualTo(value);
});
}
Пояснение к примеру:
тип
Counterопределяет счетчик, в котором создается запись с идентификаторомSAMPLE_COUNTER;команда
updateOrCreateпроверяет наличие записи счетчика, если нет, то создает новую запись с инициализирующим значением1, или увеличивает на1при наличии;выполняется чтение текущего значения счетчика командой
get;выполняется создание экземпляра типа
Sample, свойствоindexкоторого должно принимать новое значение счетчика;конечная команда
getчитает свойства созданной записи для проверки;тест выполняется три раза с проверкой того, что значение
indexсоответствует последовательности цикла.
Связывание может быть использовано совместно с вычисляемыми полями команды get. Следующий пример показывает возведение значения поля записи в верхний регистр:
@Test
void toUpperTest() {
final String name = "sample name";
final Packet createPacket = Packet.createPacket();
final ProductPartyRef productPartyRef = createPacket.productParty.create(p -> p.setName(name));
assertThatCode(() -> dataspaceCorePacketClient().execute(createPacket)).doesNotThrowAnyException();
final Packet updatePacket = Packet.createPacket();
final ProductPartyGet productPartyGet = updatePacket.productParty.get(
productPartyRef,
g -> g
.$withCalculated(
"upperName",
productPartyGrasp -> productPartyGrasp.name().upper()
)
);
updatePacket.productParty.update(
productPartyRef,
p -> p
.bind(
"name",
productPartyGet,
"/props/upperName/value"
)
);
final ProductPartyGet productPartyGetAfterUpdate = updatePacket.productParty.get(
productPartyRef,
ProductPartyWithLinkable::withName
);
assertThatCode(() -> dataspaceCorePacketClient().execute(updatePacket)).doesNotThrowAnyException();
assertThat(productPartyGet.$getCalculated("upperName", String.class)).isEqualTo(name.toUpperCase());
assertThat(productPartyGetAfterUpdate.getName()).isEqualTo(name.toUpperCase());
}
Транзакционная граница пакета#
По умолчанию в пакете команд допускается работа только с одним экземпляром агрегата, т.е. транзакционная граница пакета соответствует границе экземпляра агрегата. Для пакета явно не указывается экземпляр агрегата с которым выполняется работа. Агрегат пакета вычисляется по командам, которые его составляют. Каждая команда работает с одной сущностью, которая в свою очередь является либо корнем агрегата, либо его частью, поэтому первая команда пакета определяет агрегат пакета.
При наличии в пакете команд, работающих с разными экземплярами агрегатов, будет получена ошибка в момент выполнения: AGGREGATE_EXCEPTION
Исключение составляют команды чтения get: в рамках одного пакета команд допускается возможность получения информации о разных экземплярах агрегатов.
Расширение транзакционной границы за пределы экземпляра агрегата
Примечание
Описанное в данном разделе противоречит принципам Предметно-ориентированного проектирования (DDD) и должно применяться только в тех случаях, когда нет альтернативы.
Существует возможность расширить транзакционную границу пакета за пределы одного экземпляра агрегата. Для этого предусмотрены два варианта:
На уровне приложения Dataspace-Core. В настройке
dataspace.replication.max-aggregates-per-transactionзадается максимально допустимое количество экземпляров агрегатов в одном пакете, значение от 1 до 9999. По умолчанию 1, то есть не более одного агрегата в пакете. Настройка распространяется на все запросы к API DataSpace.На уровне конкретного запроса к API Dataspace-Core (JSON-RPC и GraphQL). Для этого в HTTP-запросе необходимо указать заголовок
X-DSPC-multiaggregate=true.
Пакет с более чем одним агрегатом имеет следующие особенности:
нельзя использовать оптимистическую блокировку, основанную на версии агрегата, вместо нее можно использовать параметр
compareкомандыupdate;идемпотентный вызов ассоциируется с агрегатом, по которому выполнена первая не читающая команда.
Для согласованной в конечном счете репликации мультиагрегатных пакетов через прикладной журнал рекомендуется использовать режим репликации только подтвержденных изменений (см. раздел «Режим ожидания подтверждения commit» в документе «Руководство по установке»).
Особенности отслеживания изменений сущностей при выполнении пакета команд Packet#
При определенных действиях в пакете команд возможны промежуточные синхронизации с БД состояния сущностей, обрабатываемых в пакете,
и как следствие - фиксация изменения, заключающееся в изменении свойства lastChangeDate, инкременте версии агрегата,
формировании событий изменения сущности (см. События отслеживания изменения),
даже если в результате выполненных команд свойствам сущности будет возвращено исходное состояние.
Примеры причин синхронизации состояния:
выполнение команды get;
выполнение команды updateOrCreate;
выполнение команды delete;
выполнение команды create, если есть поля, входящие в уникальный индекс;
выполнение команды update/updateOrCreate, если меняются коллекции reference ссылок, и/или есть поля входящие в уникальный индекс;
в мульти-агрегатном пакете при смене агрегата, т.е. когда следующая команда работает с другим агрегатом.
При обновлении такие синхронизации происходят, только если текущее состояние изменилось с момента последней синхронизации (или с момента начала обработки сущности).
Использование поискового SDK#
Внимание!
Начиная с версии dataspace-core 4.2.42 (и выше) вводится ограничение на общее количество выбираемых данных размером в 10 000. В это ограничение входят выборки корневых элементов, вложенных элементов, коллекций и подсчет количества. Если выдача по запросу превышает заданное ограничение, то генерируется исключение:
ReadRecordsCountExceededLimitException. Ограничение может быть изменено путем изменения значения параметраdataspace.readRecordsLimit.
Внимание!
При использовании пагинации необходимо учитывать, что запрос на получение очередной страницы влечет за собой новый запрос в базу данных. В связи с этим согласованное чтение не обеспечивается, так как возвращаются данные, актуальные на момент каждого обращения к базе данных.
Поисковый сервис DataSpace (на стороне сервера) позволяет выполнять динамические поисковые запросы. Под динамичностью понимается возможность формирования произвольных поисковых запросов потребителем, а не использование предопределенных запросов.
Для упрощения взаимодействия с поисковым сервисом предусмотрен SDK, который можно использовать на стороне клиента.
Взаимодействие с поисковым сервисом может осуществляться и без использования SDK.
Поисковый сервис#
Взаимодействие с серверной частью DataSpace осуществляется по протоколам JSON-RPC 2.0 через точку доступа со следующим URL-адресом: {серверURL}/search.
Для JSON-RPC точка доступа предоставляет собой метод execute, принимающий поисковый запрос в формате JSON. Пример поискового запроса можно найти во фрагменте кода:
{
"jsonrpc" : "2.0",
"method" : "execute",
"id" : "1",
"params" : {
"request" : {
"type" : "PerformedService",
"props" : [ "beginDate", "code" ],
"offset" : 15,
"limit" : 5,
"sort" : [ {
"crit" : "root.code",
"nullsLast" : false
}, {
"crit" : "root.name",
"order" : "desc"
} ],
"cond" : "root.code $like 'printSimpleRequestTest%'",
"partCond": "it.id >= '202401_' && it.id < '202402_'",
"count" : true
}
}
}
В запросе использованы следующие параметры:
type— тип искомой сущности (совпадает с именем в модели);props— перечень запрашиваемых полей и их спецификаций (в данном примере запрошены примитивные поля без спецификации);offset— смещение выборки, аналогично соответствующему оператору в SQL;limit— ограничение на количество возвращаемых результатов;sort— условие сортировки результата;crit— критерий сортировки;nullsLast— положениеnullэлементов при сортировке;cond— условие фильтрации (или условие отбора), ограничивающее выборку данных;partCond— условие для ограничения списка секций для секционированных таблиц, применяется ко всем сущностям агрегата, определенного корневым типом запроса. Не применяется при разыменовании внешней ссылки (см. раздел Ограничение выборки и сортировка).count— указывает на необходимость посчитать количество данных, удовлетворяющих условию фильтрации (без учетаlimitиoffset).
Примечание
Предполагается, что для objectId задан префикс даты (год и месяц).
Пример ответа на поисковый запрос:
{
"elems" : [ {
"type" : "PerformedService",
"id" : "202401_6849277957615255557",
"props" : {
"beginDate" : "2020-07-14T13:16:35.095",
"code" : "printSimpleRequestTesttestPerformedServiceCode1"
}
}, {
"type" : "PerformedService",
"id" : "202401_6849277961910222853",
"props" : {
"beginDate" : "2020-07-14T13:16:36.021",
"code" : "printSimpleRequestTesttestPerformedServiceCode2"
}
} ],
"count" : 17
}
Ответ содержит следующие данные:
elems— результат запроса или подзапроса;type— минимальный гарантированный тип результата (реальный тип может быть потомком и должен быть запрошен явно);id— идентификатор объекта;props— атрибуты объекта;count— количество результатов, удовлетворяющих условию фильтрации (без учетаlimitиoffset).
С подробным описанием формата запроса и ответа можно ознакомиться в документе «Протокол JSON-RPC 2.0 в применении к контроллерам модуля». С описанием формата строковых выражений можно ознакомиться в документе «Строковые выражения» (используются в условиях ограничения выборки и сортировки).
Поисковое SDK (GraphDTO)#
Поисковое SDK основано на модели потребителя и призвано упросить взаимодействие с серверной частью.
Для поиска предоставляется инструмент GraphDTO. Основная идея состоит в чтении только необходимых полей сущности, а не всех доступных.
Поисковые классы имеют названия, совпадающие с названиями сущностей модели потребителя с добавлением специфичных постфиксов:
<имя сущности>Graph — используется при создании поискового запроса и разборе ответа (служебные классы);
<имя сущности>Grasp — используется при формировании поисковых условий (раздел where в терминах sql);
<имя сущности>Get — интерфейс для работы с результатом поиска;
<имя сущности>With и <имя сущности>CollectionWith — интерфейсы для описания поискового запроса;
<имя сущности>WithPicker, <имя сущности>CollectionWithPicker и <имя сущности>GetPicker — упрощают передачу уточняющих классов в методы (классы-расширений), исключают возможность передачи некорректного класса в методы.
Классы:
GraspHelper — класс с вспомогательными функциями, применяемыми при построении поисковых условий (например,
upper().lower(),round(),ceil()и др.).DataspaceCoreSearchClient — класс, используемый для взаимодействия с серверной частью в рамках поисков (отправки поискового запроса на сервер и получения результата).
GraphCreator — класс, содержащий методы инициализации поисковых запросов для всех сущностей модели.
Создание запроса#
Создать запрос возможно одним из следующих способов:
с помощью класса GraphCreator;
с помощью соответствующего искомой сущности Graph-класса и метода createCollection.
Методы инициализации поискового запроса возвращают объект, реализующий соответствующий искомой сущности интерфейс CollectionWith.
Пример создания запроса через GraphCreator можно найти во фрагменте кода:
// Создание запроса для сущности Product
ProductCollectionWith psWith = GraphCreator.selectProduct();
// Создание запроса для сущности PerformedService
PerformedServiceCollectionWith psWith = GraphCreator.selectPerformedService();
Пример создания запроса через Graph-классы можно найти во фрагменте кода:
// Создание запроса для сущности Product
ProductCollectionWith psWith = ProductGraph.createCollection();
// Создание запроса для сущности PerformedService
PerformedServiceCollectionWith psWith = PerformedServiceGraph.createCollection();
Объявление запрашиваемых данных#
Для объявления запрашиваемых данных (данных, которые необходимо получить с сервера) необходимо после создания запроса воспользоваться соответствующими методами, начинающимися с префикса with.
Например, для получения атрибута name необходимо вызвать метод withName().
Примечание
Получение идентификатора объекта — особый случай. Идентификатор объекта возвращается при каждом запросе. Запрашивать идентификатор объекта явно не требуется, а метод его запроса отсутствует.
При вызове метода работает «цепочка вызовов», т.е. очередной метод можно вызывать сразу после вызова предыдущего без использования промежуточных переменных.
Запрос примитивных полей#
Пример создания запроса примитивных полей:
PerformedServiceCollectionWith psWith = GraphCreator.selectPerformedService()
.withName()
.withCode();
В примере результат сформированного запроса присваивается переменной, при помощи которой можно осуществлять последующую корректировку (донасыщение) запроса.
Запрос одиночного (единственного) объекта#
Если заранее известно, что запрос должен вернуть ровно один объект, можно воспользоваться методом поискового клиента SDK, который начинается с get и оканчивается на тип искомой сущности (например, dataspaceCoreSearchClient.**getProductParty**).
Первый параметр метода принимает запрос на поиск.
Второй параметр переопределяет поведение API при отсутствии в БД значения, удовлетворяющего условию поиска. При выставленном флаге вместо исключения ObjectNotFoundException возвращается значение «null».
API генерирует следующие исключения:
TooManyResultsException— если запрос вернул более одного значения.ObjectNotFoundException— если результат запроса пуст и второй параметр API не задан, либо задано значение «false».
Пример запроса:
ProductPartyGet productPartyGet = dataspaceCoreSearchClient.getProductParty(pp ->
pp.withCode()
.withName()
.setWhere(where -> where.codeEq(code1))
, true);
ProductPartyGet productPartyGet2 = dataspaceCoreSearchClient.getProductParty(pp ->
pp.withCode()
.withName()
.setWhere(where -> where.idEq(someId))
);
Запрос вложенных объектов и коллекций объектов#
При запросе вложенного объекта (ссылочное поле) или коллекции объектов (коллекция ссылок) необходимо указывать внутреннюю сигнатуру запроса (описать возвращаемые данные и ограничения в случае коллекций). Эту структуру необходимо задать через лямбда-функцию.
Функция заполняется по тем же правилам, что и родительский объект. Единственное отличие — на вход передается созданный объект запроса, который необходимо дополнить требуемыми полями.
Пример создания запроса с вложенным объектом можно найти во фрагменте кода:
GraphCreator.selectPerformedService()
.withCode()
.withName()
// запрос ссылочного поля
.withProduct(ppWith->
// задаем какие данные возвращать
ppWith.withSeries()
.withNum()
.withBeginDate())
// запрос коллекции ссылок
.withPerfomedOperations(poWith ->
// задаем какие данные возвращать
poWith.withCode()
.withSummaOperation()
.withBeginDate());
Битые ссылки#
Битая ссылка — ссылка, по идентификатору которой отсутствует сущность в базе данных.
Если ссылка на объект является битой, то на этом объекте метод $isBrokenLink() вернет значение true, а методы получения полей будут выбрасывать исключения (контроль битых ссылок).
Если есть необходимость получить идентификатор битой ссылки, то на этом объекте можно вызвать метод $getBrokenLink().
Ниже представлен пример вызова методов $isBrokenLink() и $getBrokenLink()
GraphCollection<DepositCBReplenishGet> result = dataspaceCoreSearchClient().depositCBReplenish.search(dep -> {
dep
.withExternalDocumentsOnline(doc -> doc.withReference(ref -> ref.withName().withCode()))
.setWhere(depos -> depos.terBankName().eq(terBankName));
});
result.forEach(dep -> {
dep.getExternalDocumentsOnline().forEach(doc -> {
DocumentOnlineGraph docOnline = (DocumentOnlineGraph) doc.getReference().getEntity();
assertTrue(docOnline.$isBrokenLink());
assertEquals(docOnline.$getBrokenLink(), "ABCDEF123456789");
});
});
Запрос идентификаторов вложенных объектов и коллекций объектов#
Если необходимо получить только идентификатор вложенного объекта или коллекции вложенных объектов, то необходимо вызвать соответствующий метод with без параметров. Для коллекции объектов можно передать лямбду с настройкой ограничивающих условий и фильтрации (рассматривается далее).
Иными словами, если спецификация сущности не передана или не содержит возвращаемых данных, то по умолчанию всегда возвращается идентификатор сущности.
Пример создания запроса с вложенным объектом:
GraphCreator.selectPerformedService()
.withCode()
.withName()
// запрос идентификатора вложенной сущности
.withProduct()
// запрос идентификаторов сущностей из коллекции
.withPerfomedOperations();
Доступные для поиска системные поля#
Системные поля — поля, которые предоставлены пользователю функциональностью продукта DataSpace.
Системные поля, доступные для поиска:
objectId;type;lastChangeDate;chgCnt.
Уточнение типа вложенного объекта и коллекции объектов#
Ссылочные поля (и коллекции ссылок) могут «ссылаться» не на базовый класс, а на класс-потомок. Например, поле Product может ссылаться не на ProductParty, а на его потомка — DepositCBExmpl.
В такой ситуации имеется возможность уточнить тип ссылочного поля (или коллекции ссылок). Для запроса ссылочного поля или коллекции ссылок с уточнением типа используются перегруженные методы с такими же названиями, что и при запросе без уточнения типа. При этом уточняющий тип передается первым параметром метода, а лямбда — вторым. При этом в лямбде появляются методы для запроса полей указанного потомка.
Пример создания запроса на класс расширения:
GraphCreator.selectPerformedService()
.withCode()
.withName()
.withProduct(ProductPartyCollectionWIthPicker::DepositCBExmpl, ppWith->
ppWith.withDeclaration()
.withLastCptDate())
.withPerformedOperations(PerfromedOperationCollectionWithPicker::BigOperation, poWith ->
poWith.withCode()
.withBigCost());
Вычитка запрошенного поля:
GraphCollection<PerformedServiceGet> res = dataspaceCoreSearchClient.searchPerformedService(req);
DepositCBExmplGet deposit = res.get(0).getProduct(ProductPartyGetPicker::DepositCBExmpl);
String declaration = deposit.getDeclaration();
Date date = deposit.getLastCptDate()
GraphCollection<BigOperationGet> operations = res.get(0).getPerformedOperations(PerfromedOperationGetPicker::BigOperation);
BigDecimal bigCost = operations.get(0).getBigCost();
Детализация коллекции объектов по потомкам#
Если в коллекции (корневой или вложенной) лежат объекты разных типов (базовый класс и его потомки), то имеется возможность одним запросом выбрать объекты произвольных типов. Для этого необходимо запросить для коллекции минимально необходимый базовый класс (объединяющий всех потомков) и затем детализировать выборку по его потомкам.
В примере ниже запрашиваются объекты типа DepositCBReplenish. При этом, если объект является не просто DepositCBReplenish, а потомком типа DepositCBReplenishPlus, то необходимо дополнительно выбрать указанные атрибуты этого потомка.
Детализация корневой коллекции объектов по потомкам
Пример запроса:
GraphCreator.selectDepositCBReplenish()
.withTerBankCode()
.withTerBankName()
// Детализируем запрашиваемые данные по потомкам.
// Первым параметром указываем тип потомка DepositCBReplenish,
// вторым — описываем характерные для потомка данные.
.extend(DepositCBReplenishWithPicker::DepositCBReplenishPlus, p -> p
.withTerBankNameOnline()
.withTerBankNamePlus()
);
Работа с ответом:
GraphCollection<DepositCBReplenishGet> result = dataspaceCoreSearchClient.searchDepositCBReplenish(req);
// Можно либо каждый элемент коллекции проверить на то, является ли он нужным потомком (обязательно постфикс Get)
for (DepositCBReplenishGet item : resul) {
if (item instanceof DepositCBReplenishPlusGet) {
((DepositCBReplenishPlusGet) item).getTerBankNameOnline();
}
}
// Либо отфильтровать из коллекции сразу нужные типы
// В resultPlus попадут только элементы, являющиеся DepositCBReplenishPlusGet или его потомками
Collection<DepositCBReplenishPlusGet> resultPlus = result.getCollection(DepositCBReplenishPlusGet.class);
Детализация вложенной коллекции объектов по потомкам
В примере ниже для корневого объекта запрашивается вложенная коллекция элементов типа ProductRegisterDepositCBReplenish. При этом для дочерних элементов типов ProductRegisterDepositCBReplenishMainWith и ProductRegisterDepositCBReplenishAdditionalWith запрашивается разный набор данных. Для выполнения этого запроса ProductRegisterDepositCBReplenishMainWith и ProductRegisterDepositCBReplenishAdditionalWith необязательно должны находиться в одной цепочки иерархии, но должны иметь общего предка — ProductRegisterDepositCBReplenish.
Пример запроса:
GraphCreator.selectDepositCBReplenish()
.withTerBankCode()
.withTerBankName()
// Запрашиваем вложенную коллекцию, уточняем базовый запрашиваемого тип объекта
.withProductRegisters(ProductRegisterPicker::ProductRegisterDepositCBReplenish, p -> p
.withRegisterNumber()
.withFirstValue()
// Детализируем тип объекта вложенной коллекции по потомкам
// Оба типа расширяют тип ProductRegisterDepositCBReplenish
.extend(ProductRegisterDepositCBReplenishMainWith.class,
g -> g.withCodeMain())
.extend(ProductRegisterDepositCBReplenishAdditionalWith.class,
g -> g.withCodeAdd())
);
Работа с ответом:
GraphCollection<DepositCBReplenishGet> result = dataspaceCoreSearchClient.searchDepositCBReplenish(req);
// Получаем полную коллекцию, приведенную к заданному типу
GraphCollection<ProductRegisterDepositCBReplenishGet> result1
= result.get(0).getProductRegisters(ProductRegisterhGetPicker::ProductRegisterDepositCBReplenishGet);
// Получаем подколлекцию, состоящую из потомков ProductRegisterDepositCBReplenishMainGet
GraphCollection<ProductRegisterDepositCBReplenishMainGet> mainResult
= result.get(0).getProductRegisters(ProductRegisterhGetPicker::ProductRegisterDepositCBReplenishMainGet);
// Получаем подколлекцию, состоящую из потомков ProductRegisterDepositCBReplenishAdditionalGet
GraphCollection<ProductRegisterDepositCBReplenishAdditionalGet> additionalResult
= result.get(0).getProductRegisters(ProductRegisterhGetPicker::ProductRegisterDepositCBReplenishAdditionalGet);
// Как и в предыдущем примере можно выполнять проверки на instanceof
Пример сортировки по полю потомка класса по которому осуществляется выборка
В представленном ниже примере запрашиваются корневой объект и его потомки. В конце запроса выполняется сортировка по свойству
code одного из потомков - ProductExt1. При этом следует отметить, что в связи с особенностями параметризации данного запроса,
необходимо указать конкретный тип для параметризованной коллекции - <ProductGrasp> вместо <? extends ProductGrasp>.
Также требуется явное преобразование типа при определении такой коллекции -
(ProductCollectionWith) ProductGraph.createCollection(). После этого появится возможность установить желаемую сортировку для запроса.
ProductCollectionWith<ProductGrasp> req = (ProductCollectionWith) ProductGraph.createCollection();
req // запрашиваем свойства у родителя
.withName()
.withCode()
// запрашиваем у потомков
.extend(ProductExt1With.class, p -> p.withName())
.extend(ProductExt2With.class, p -> p.withName())
.extend(ProductExt3With.class, p -> p.withName().withCode())
.setSortingAdvanced(sort ->
sort.desc(
// выполняем сортировку по свойству 'code' потомка - ProductExt1
ProductExt1Grasp.class,
picker -> picker.code(),
SortNullsBehavior.NULLS_LAST
));
GraphCollection<ProductGet> res = searchClient.product.search(req);
Установка ограничений на вложенную коллекцию#
При запросе коллекционных данных имеется возможность задать ограничивающие условия и критерии сортировки. Ограничивающие условия и критерии сортировки подробно будут рассмотрены ниже.
Пример запроса с ограничением вложенных выбираемых данных:
GraphCreator.selectPerformedService()
.withCode()
.withName()
.withPerformedOperations(psWith->
psWith.withCode()
.withBeginDate()
// задаем условие фильтрации (ограничивающие условия будут детально рассмотрены ниже)
.setWhere(psGrasp -> psGrasp.codeLike("abc%"))
// задаем количество возвращаемых записей
.setLimit(10)
// задаем количество пропускаемых объектов
.setOffset(20)
// задаем сортировку
.setSortingAdvanced(sortBuilder -> sortBuilder
.asc(PerformedOperationGrasp::code, SortNullsBehavior.NULLS_LAST)
// альтернативный вариант указания поля сортировки
.desc(o -> 0.name(), SortNullsBehavior.NULLS_FIRST)
)
);
Запрос примитивной коллекции#
Запрос примитивной коллекции осуществляется таким же способом, как и запрос коллекции ссылок. Единственное отличие — невозможность указания возвращаемых данных, т.к. элемент коллекции — примитив, у которого нет полей или вложенных объектов. При этом остается возможность задания ограничивающих условий и сортировки.
Пример создания запроса примитивной коллекции:
GraphCreator.selectPerformedService()
.withCode()
.withName()
// Получение примитивной коллекции целиком
.withStates();
GraphCreator.selectPerformedService()
.withCode()
.withName()
// Получение примитивной коллекции по условию фильтрации
.withStates(states ->
states.setWhere(elem -> elem.like("prefix%"))
);
Запрос цепочки вложенных объектов#
Запрашивать вложенные объекты можно не только для корневой сущности, но и для вложенных сущностей, а также коллекций сущностей.
Пример запроса цепочки объектов:
GraphCreator.selectPerformedService()
.withCode()
.withName()
// запрос вложенного в сервис продукта
.withProduct(ppWith ->
// запрос простого поля code
ppWith.withCode()
// запрос коллекции вложенных операций для вложенного в сервис продукта
.withPerformedOperations(poWith ->
// описание возвращаемых данных
poWith.withCode()
.withBeginDate()
// условие фильтрации операций внутри продукта
.setWhere(where -> where.codeLike("codePrefix%"))
)
);
Разыменование внешних ссылок#
Под разыменованием внешних ссылок понимается использование в запросах объектов (и их атрибутов), которые находятся за внешними ссылками, но физически расположены в текущей БД.
Синтаксис разыменования отличается от места его использования (в запросе данных или в фильтрации, сортировке).
При запросе данных для разыменования внешней ссылки используется соответствующий ссылке перегруженный метод with<Имя внешней ссылки>(<Запрос данных>):
GraphCreator.selectPerformedOperation()
.withCode()
// Product — внешняя ссылка. Перегруженный метод принимает лямбду,
// в которой можно задавать спецификацию для разыменованной ссылки
.withProduct(productWithLinkable -> productWithLinkable.withCode())
Запрос коллекции внешних ссылок с сортировкой и ограничением по количеству:
GraphCreator.selectProductParty()
.withCode()
// externalOperations — коллекция внешних ссылок. Т.к. это коллекция, то внешняя ссылка обернута объектом с backReference
// Для разыменования коллекционной ссылки используем метод .withReference(), в который передаем спецификацию запроса
// Для разыменования ссылки в сортировке используем вызов .reference().entity()
.withExternalOperations(op -> op.withReference(op2 -> op2.withCode())
.setSortingAdvanced(sort -> sort.desc(op2 -> op2.reference().entity().code()))
.setLimit(1))
// ограничение выбираемых продуктов по коду
.setWhere(where -> where.codeIn(testCodeProduct1, test
CodeProduct2));
При фильтрации или сортировке данных для разыменования необходимо использовать метод entity() на ссылке. Этот метод предоставляет доступ к атрибутам соответствующей сущности.
GraphCreator.selectPerformedOperation()
.withCode()
// В условии фильтрации на объекте с внешней ссылкой на ссылке появляется виртуальный метод с именем entity,
// по которому можно построить условие
.setWhere(where -> where.product().entity().codeEq(testCodeProduct2));
Примечание
Типизация внешних ссылок и сортировка по коллекции внешних ссылок (с использованием агрегатных функций) находится в разработке.
Примечание
Условие
partCond, используемое для ограничения списка секций для секционированных таблиц, не применяется при разыменовании внешних ссылок. Смотри раздел Ограничение выборки и сортировка.
Использование расчетных полей#
Расчетные поля — это поля, которые отсутствуют на объекте и позволяют запросить некоторую дополнительную информацию.
Расчетные поля используются, например, для получения части коллекции, удовлетворяющей определенным условиям (например, получение действующих и завершенных сервисов в разных коллекциях).
Расчетные поля могут быть следующих видов:
Примитивное расчетное поле — поле, значением которого является «примитивный» тип (String, Long, Integer, Date, OffsetDateTime и т.п.). Такое расчетное поле может использоваться, например, для получения размера вложенной коллекции, определения максимального или минимального значения поля в коллекции и др.
Расчетное поле на базе внутренней ссылки — поле, значением которого является объект того же типа, что и поле. Пример использования: получение вложенного объекта под другим именем поля (алиас).
Расчетное поле на базе коллекции внутренних ссылок — поле, это поле, содержащее коллекцию элементов, совпадающую по типу с самим полем.
Примечание
В формировании расчетных полей не может участвовать тип Binary.
Запрос расчетного поля#
Запрос расчетного поля с «примитивным» типом осуществляется при помощи метода .$withCalculated().
Пример запроса расчетных полей, имеющих «примитивный» тип:
GraphCreator.selectProductParty()
// запрашиваем поле "код" объекта
.withCode()
// запрашиваем дополнительные расчетные поля:
// запрашиваем расчетное поле, значением которого является конкатенация кода и имени продукта
.$withCalculated("codeAndName", grasp -> grasp.code().concat(grasp.name()))
// запрашиваем количество сервисов, связанных с объектом, personnelNumber которых больше или равен трем
.$withCalculated("servicesCountGt2", it -> it.performedServicesCount(serv -> serv.personnelNumberGreaterOrEq(3L)))
// запрашиваем количество сервисов, связанных с объектом, personnelNumber которых меньше двух
.$withCalculated("servicesCountLess2", it -> it.performedServicesCount(serv -> serv.personnelNumberLess(2L)))
// запрашиваем максимальный код сервиса среди сервисов, связанных с объектом, чей personnelNumber меньше 4-х
.$withCalculated("maxServiceCode", it -> it.performedServicesMax(serv -> serv.code(),
where -> where.personnelNumberLess(4L)))
// ограничение выборки продукта
.setWhere(pp -> pp.codeEq(ppCode));
Пример запроса расчетных полей, имеющих тип «коллекция»:
ProductPartyCollectionWith<ProductPartyGrasp> request = GraphCreator.selectProductParty()
.withCode()
// для запроса расчетного поля на базе коллекции используется перегруженный метод запроса коллекционного поля,
// первый параметр которого задает называние расчетного поля, а второй специфику запроса коллекции
// в данном примере под именем "servicesGreater2" запрашивается подколлекция сервисов с personnelNumber превышающим 2
.withPerformedServices("servicesGreater2",
it -> it.withCode()
.withPersonnelNumber()
.setWhere(where -> where.personnelNumberGreater(2L))
)
// здесь под именем "servicesLessOrEq2" запрашивается подколлекция сервисов с personnelNumber меньшим или равным 2
.withPerformedServices("servicesLessOrEq2",
it -> it.withCode()
.withPersonnelNumber()
.setWhere(where -> where.personnelNumberLessOrEq(2L))
)
.setWhere(where -> where.codeEq(prefix));
Получение результата запроса расчетных полей#
Обращение к результату вычисления расчетного поля с «простым типом» осуществляется при помощи метода .$getCalculated(), в первом параметре которого указывается имя расчетного поля, а во втором — тип результата:
GraphCollection<ProductPartyGet> result = dataspaceCoreSearchClient.searchTestTypeField(request);
result.get(0).$getCalculated("codeAndName", String.class)
result.get(0).$getCalculated("servicesCountGt2", Long.class)
result.get(0).$getCalculated("servicesCountLess2", Long.class)
result.get(0).$getCalculated("maxServiceCode", String.class)
Получение результата расчетных полей, имеющих тип «коллекция»:
GraphCollection<PerformedServiceGet> servicesGreater2 = result.get(0).getPerformedServices("servicesGreater2");
GraphCollection<PerformedServiceGet> servicesLessOrEq2 = result.get(0).getPerformedServices("servicesLessOrEq2");
Пример получения результатов расчетных полей разных типов:
result.get(0).$getCalculated("boolf", Boolean.class);
result.get(0).$getCalculated("bytef", Byte.class);
result.get(0).$getCalculated("charf", Character.class);
result.get(0).$getCalculated("shortf", Short.class);
result.get(0).$getCalculated("intf", Integer.class);
result.get(0).$getCalculated("longf", Long.class);
result.get(0).$getCalculated("floatf", Float.class);
result.get(0).$getCalculated("doublef", Double.class);
result.get(0).$getCalculated("bigDecimalf", BigDecimal.class);
result.get(0).$getCalculated("datef", Date.class);
result.get(0).$getCalculated("localDatef", LocalDate.class);
result.get(0).$getCalculated("localDateTimef", LocalDateTime.class);
result.get(0).$getCalculated("offsetDateTimef", OffsetDateTime.class);
result.get(0).$getCalculated("strf", String.class));
result.get(0).$getCalculated("textf", String.class));
Сортировка по расчетным полям#
Только примитивные расчетные поля могут участвовать в сортировке.
Для сортировки по расчетному полю необходимо указать название расчетного поля, используемого в запросе. Если переданное в сортировке название поля не будет
совпадать ни с одним из названий, используемых в запросе расчетных полей, то в процессе выполнения будет выброшено исключение StringSortingOnNonCalculatedFieldException.
GraphCreator.selectProductParty()
// запрашиваем поле "код" объекта
.withCode()
// запрашиваем расчетное поле, значением которого является конкатенация кода и имени продукта
.$withCalculated("codeAndName", grasp -> grasp.code().concat(grasp.name()))
// в сортировке указываем название, используемое при объявлении расчетного поля
.setSortingAdvanced(sort -> sort.desc("codeAndName"))
// ограничение выборки продукта
.setWhere(pp -> pp.codeEq(ppCode));
Использование distinct-запросов#
Distinct-запросы отличаются от «традиционных» запросов DataSpace тем, что результат такого запроса является не объектом, а набором вычислимых полей. При этом формирование distinct-запроса основано на типе модели предметной области.
Формирование distinct запроса начинается с вызова метода .createSelection() на Graph-классе сущности модели.
После этого при помощи методов .$withCalculated осуществляется запрос данных.
В запросе (в любом месте) необходимо вызвать метод .distinct(), который означает, что запрос выбирает уникальные записи.
При необходимости может быть добавлено: условие фильтрации данных .where(); условие пагинации .setLimit() или setOffset(); запрос общего количества .setTotalCount(true).
Для выполнения запроса необходимо на поисковом клиенте вызвать метод .selectionSearch(), передав в него объект запроса.
Результат запроса представляет собой коллекцию типа GraphCollection<Selection>. Для получения конкретного значения
необходимо выбрать из коллекции необходимый элемент и вызвать на нем метод .$getCalculated(), передав название расчетного поля и его тип.
Пример построения distinct-запроса и разбора ответа:
SelectionWith<? extends ProductPartyGrasp> request = ProductPartyGraph.createSelection()
.$withCalculated("name", it -> it.name())
.setWhere(where -> where.codeLike(prefix + "%"))
.distinct();
// выполнение запроса
GraphCollection<Selection> result = dataspaceCoreSearchClient().selectionSearch(request);
// получение результата
String uniqueName = result.get(0).$getCalculated("name", String.class);
Сортировка в distinct-запросах#
Сортировка в distinct-запросах осуществляется таким же образом, как и сортировка по расчетным полям.
Получение идентификаторов внешних ссылок для одиночной ссылки и коллекции внешних ссылок#
Для получения идентификатора внешних ссылок для одиночной ссылки и коллекции внешних можно вызвать следующий поисковый сервис:
//вызываем поисковой сервис
GraphCollection<ProductGet> result = dataspaceCoreSearchClient.searchProduct(collectionWith -> collectionWith
.withOwner()
.withRequests(req -> req.withReference()));
//получаем результат
String ownerID = result.get(0).getOwner().getEntityId();
Аналогично для получения идентификаторов внешних ссылок на коллекцию.
Использование GroupBy в запросах#
Запросы с использованием GroupBy — разновидность запросов с вычислимыми полями. Поэтому рекомендуется изучить главы про вычислимые поля и Distinct-запросы.
Формирование запроса с GroupBy начинается с вызова метода .createSelection() на классе Graph сущности модели.
Затем при помощи методов .$addGroupBy,.$withGroup и .$having осуществляется формирование запроса:
.$addGroupBy— добавление в запрос поля, по которому будет осуществляться группировка. Для группировки по нескольким полям необходимо несколько раз вызывать данный метод..$withGroup— добавление агрегирующей функции, которая будет вычисляться для каждой группы..$having— задание условия для выборки по группам.
Вызов и вычитка значений осуществляется аналогично Distinct-запросам. Пример построения запроса с GroupBy и разбор ответа:
Packet packet = new Packet();
BookStoreRef bookStoreRef = packet.bookStore.create(createBookStoreParam -> createBookStoreParam
.setAddress("Невский пр., 28, Санкт-Петербург"));
dataspaceCorePacketClient.execute(packet);
packet = new Packet();
packet.book.create(createBookParam -> createBookParam
.setBookStore(bookStoreRef)
.setAuthor("А.С.Пушкин")
.setName("Капитанская дочка"));
dataspaceCorePacketClient.execute(packet);
packet = new Packet();
packet.book.create(createBookParam -> createBookParam
.setBookStore(bookStoreRef)
.setAuthor("А.С.Пушкин")
.setName("Повести Белкина"));
dataspaceCorePacketClient.execute(packet);
packet = new Packet();
packet.book.create(createBookParam -> createBookParam
.setBookStore(bookStoreRef)
.setAuthor("М.Ю.Лермонтов")
.setName("Мцыри"));
dataspaceCorePacketClient.execute(packet);
packet = new Packet();
packet.book.create(createBookParam -> createBookParam
.setBookStore(bookStoreRef)
.setAuthor("М.Ю.Лермонтов")
.setName("Герой нашего времени"));
dataspaceCorePacketClient.execute(packet);
packet = new Packet();
packet.book.create(createBookParam -> createBookParam
.setBookStore(bookStoreRef)
.setAuthor("А.И.Куприн")
.setName("Гранатовый браслет"));
dataspaceCorePacketClient.execute(packet);
SelectionWith<? extends BookGrasp> bookSelection = BookGraph.createSelection()
// Добавляем группировку по автору
.$addGroupBy(groupBy -> groupBy.author())
// Добавляем автора
.$withGroup("authorValue", groupSelector -> groupSelector.none(bookGrasp -> bookGrasp.author()))
// Добавляем количество книг по автору
.$withGroup("booksCount", groupSelector -> groupSelector.count(bookGrasp -> bookGrasp.name()))
// Из всех авторов выбираем только тех, у которых больше 1 книги
.$having(groupSelector -> groupSelector.count(bookGrasp -> bookGrasp.name()).greater(1))
// Сортируем по количеству книг по убыванию
.setSortingAdvanced(advancedSortBuilder -> advancedSortBuilder.desc("authorValue"));
GraphCollection<Selection> selections = dataspaceCoreSearchClient.selectionSearch(bookSelection);
Selection selection0 = selections.get(0);
Assertions.assertEquals("М.Ю.Лермонтов", selection0.$getCalculated("authorValue", String.class));
Assertions.assertEquals(2, selection0.$getCalculated("booksCount", Integer.class));
Selection selection1 = selections.get(1);
Assertions.assertEquals("А.С.Пушкин", selection1.$getCalculated("authorValue", String.class));
Assertions.assertEquals(2, selection1.$getCalculated("booksCount", Integer.class));
Сортировка в запросах с использованием GroupBy#
Сортировка в запросах с использованием GroupBy осуществляется таким же образом, как и сортировка по расчетным полям.
Допустимые ограничивающие операции с полями binary и text#
Text-поля
Пример модели с text-полями:
...
<class name="Product" label="Базовый продукт клиента">
<property name="clob1" type="text"/>
<property name="clob2" type="Text"/>
</class>
...
Ограничивающие операции, допустимые с данным типом полей:
...
ProductCollectionWith productCollectionWith = ProductGraph.createCollection()
.withClob1()
.setWhere(
params -> params
.clob1Like("test")
.and(params.clob1NotLike("test1"))
.and(params.clob1IsNotNull())
.or(params.clob1IsNull())
);
...
Остальные типы операций не доступны с данным типом поля на SDK (такие как LESS, LESS_OR_EQ, GREATER, GREATER_OR_EQ, BETWEEN).
Операция EQ может быть реализована через операцию LIKE.
Binary-поля
Пример модели с binary-полями:
...
<class name="Product" label="Базовый продукт клиента" implements="WithCodeAndName" lockable="true">
<property name="blob1" type="byte[]"/>
<property name="blob2" type="binary"/>
<property name="blob3" type="Binary"/>
</class>
...
Запрос количества записей#
Если необходимо получить количество записей по классу, то требуется выставить параметр setTotalCount(true).
Для получения результата нужно вызвать метод getTotalCount().
При вычислении количества записей учитывается только условие фильтрации записей (условие where) и не учитываются ограничения на количество выбираемых записей и смещение (limit, offset). Использование where, limit, offset описано ниже
в данном документе.
SDK:
Packet packet1 = new Packet();
packet1.product.create(it -> it.setCode("code1"));
packetClient.execute(packet1);
Packet packet2 = new Packet();
packet2.product.create(it -> it.setCode("code2"));
packetClient.execute(packet2);
Query1CollectionWith<Query1Grasp> req = Query1Graph.createCollection()
.setTotalCount(true)
// метод ограничения выборки рассматривается далее
.setWhere(where -> where.codeEq("code1"));
GraphCollection<Query1Get> res = searchClient.query1.search(req);
Запрос версии агрегата (setNeedAggregateVersion)#
Пример запроса версии агрегата показан в следующем фрагменте кода:
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.codeLike("codePrefix%"))
// установка признака необходимости получения версии агрегата для корневого объекта
// на вложенных объектах данная функция не поддерживается и выбросит исключение в runtime
.setNeedAggregateVersion(true);
// Получение результата на объекте ответа
ProductPartyGet result = ... //Получение ответа — рассматривается ниже
Long aggregateVersion = result.$getAggregateVersion();
Примечание
Также возможно получить версию агрегата через прямую и обратные ссылки, а также коллекции этих ссылок.
// Запрашиваем версии агрегата для ссылочных полей и коллекций ссылок
GraphCreator.selectProductParty()
.withProductCheckedRef(prod -> prod.setNeedAggregateVersion(true))
.withProductCheckedRefs(prodLink -> prodLink.withReference(prod -> prod.setNeedAggregateVersion(true)))
.withExternalService(serv -> serv.withExternalProduct(prod -> prod.setNeedAggregateVersion(true)).setNeedAggregateVersion(true))
.withExternalServices(servCollect -> servCollect.withReference(serv -> serv.setNeedAggregateVersion(true)))
.setNeedAggregateVersion(true);
Примечание
При запросе версии агрегата через механизм разыменовывания внешних ссылок следует учесть, что для получения корректного значения необходимо разносить по разным пакетам создание/изменение внешней ссылки и получение значения версии агрегата.
Допустимых ограничивающих операций в SDK для данного типа поля нет!
Ограничение выборки и сортировка#
Для ограничения выборки и сортировки используются методы интерфейса AbstractProxyCollectionWith, начинающиеся с префикса set.
К таким методам относятся:
setWhere— устанавливает условие фильтрации данных.setPartCond— устанавливает условие для ограничения списка секций для секционированных таблиц.setLimit— ограничение на количество возвращаемых элементов.setOffset— смещение запрашиваемых данных.setSortingAdvanced— задает любые условия сортировки, включая атрибуты корневой, вложенных сущностей, результатов агрегирующих функций.setTotalCount— маркер/ информирующий о необходимости подсчета общего количества элементов, удовлетворяющих условию фильтрации данных (без учета limit и offset).setNeedAggregateVersion— устанавливает признак необходимости получения версии агрегата для корневых объектов выборки. Получение результата осуществляется через метод $getAggregateVersion(). Есть возможность запросить версию агрегата через прямую и обратную ссылки, а также коллекции этих ссылок.
Метод setWhere (ранее метод setGrasp) позволяет задать условие фильтрации сущностей. Условие фильтрации задается при помощи лямбды (в которую передается Grasp-объект). Пример поиска с ограничивающими условиями приведен ниже.
Метод setPartCond позволяет задать условие для ограничения списка секций для секционированных таблиц.
Допускается вызов метода setPartCond только на корневом элементе запроса. Вызов метода на вложенных элементах запроса приведет к ошибке во время выполнения.
Данное условие применяется для всех классов (таблиц) в рамках агрегата, определенного корневым типом запроса.
Условие для ограничения списка секций для секционированных таблиц не распространяется на разыменование внешних ссылок.
Внимание!
В методе
setPartCondиспользуется лямбда для построения условия. В лямбду передается тот же объект, что и для методаsetWhere. Несмотря на то, что данный объект позволяет строить условия с использованием различных полей класса, на котором строится запрос, в методеsetPartCondподдерживаются только условия по идентификатору сущности. В противном случае возникнет исключение в runtime.
Примечание
Информация о применении секционирования приведена в документе Структура базы данных в разделе «Механизм вытеснения данных из оперативной базы».
Примечание
Предполагается, что для objectId задан префикс даты (год и месяц).
GraphCreator.selectProductParty()
.withName()
.withCode()
.withPerformedServices(PerformedServiceCollectionWIthPicker::DepositOpenSrvCBExmpl, psWith->
psWith.withChannel()
.withBeginDate()
// ограничение вложенной коллекции
.setWhere(psWhere -> psWhere.codeLike("someCode%"))
.setLimit(25))
// ограничение корневой искомой сущности
.setWhere(ppWhere -> ppWhere.nameLike("someName%"))
.setPartCond(partCond -> partCond.id().greater("202401_").and(partCond.id().less("202402_")))
.setLimit(10)
.setOffset(20)
.setTotalCount(true)
.setSortingAdvanced();
В примерах ниже основное внимание уделяется методу setWhere и способам построения условий фильтрации.
Условия фильтрации формируются при помощи параметра лямбды, имеющего тип <имяСущности>Grasp (например, PerformedServiceGrasp). Чтобы задать условие фильтрации, необходимо поставить точку после параметра лямбды и начать ввод искомого поля. Среда разработки подскажет допустимые операции с этим полем (Eq, NotEq, Like, NotLike, Greater, Lower и т.п.), как показано на рисунке:

Ограничение выборки по идентификатору объекта#
Вне зависимости от названия поля идентификатора на физическом уровне в GraphDTO поле идентификатора именуется «id». Соответственно все методы ограничения идентификатора начинаются с префикса «id».
Пример фильтрации по идентификатору:
GraphCreator.selectProductParty()
.withCode()
// выбираем все объекты, чей идентификатор находится в заданном списке
.setWhere(ppWhere -> ppWhere.idIn("1", "2", "3"));
Ограничение выборки по примитивному параметру#
Пример фильтрации по примитивному полю:
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.codeLike("codePrefix%"));
Инверсия условия (not)#
Изменить условие на противоположное можно следующими способами:
использовать противоположный метод;
обернуть условие во вспомогательную функцию.
В примере ниже для метода codeLike() противоположное условие задается методом codeNotLike():
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.codeNotLike("codePrefix%"));
Во фрагменте кода ниже оригинальное условие обернуто во вспомогательную функцию GraspHelper.not():
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> GraspHelper.not(ppWhere.codeLike("codePrefix%")));
Выборка с проверкой поля на null#
На значение «null» можно проверить только примитивные свойства и ссылки. Для коллекций должна быть проверка по равенству количества элементов нулю.
Пример проверки поля на значение «null»:
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.nameIsNull());
Ограничение примитивного поля с использованием атрибутов самой сущности#
Пример фильтрации с использованием атрибутов сущности:
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.codeEq(ppWhere.name()));
Ограничение примитивного поля с использованием атрибутов вышестоящих сущностей#
В рамках агрегатоцентричности у «нижестоящей» сущности имеется ссылка на вышестоящую, через которую можно обратиться к ее атрибутам. Пример обращения к вышестоящей сущности через ссылку:
// Для продуктов запрашиваем сервисы, а для сервисов операции, имена которых начинаются с имени соответствующего сервиса.
GraphCreator.selectProductParty()
.withCode()
.withPerformedServices(servGraph -> servGraph
.withCode()
.withPerformedOperations(opGraph -> opGraph
.withAmount()
// имя операции должно начинаться с именем сервиса, которому она принадлежит
.setWhere(opWhere -> opWhere.nameLike(opWhere.service().name().concat("%")))
)
);
В некоторых случаях может не быть подходящей ссылки на вышестоящую сущность, например, если связаны две «параллельные» сущности в рамках одного агрегата. Если нет подходящей ссылки на родительскую сущность, можно воспользоваться специализированным методом $link на соответствующем Graph-объекте, как показано в примере:
// Для продуктов запрашиваем сервисы, а для сервисов операции, имена которых начинаются с имени соответствующего сервиса.
GraphCreator.selectProductParty()
.withCode()
.withPerformedServices(servGraph -> servGraph
.withCode()
.withPerformedOperations(opGraph -> opGraph
.withAmount()
// обращение к сервису через Graph переменную и метод $link()
.setWhere(opWhere -> opWhere.nameLike(servGraph.$link().name().concat("%")))
)
)
Чтобы обратиться к атрибутам корневого объекта через $link(), необходимо сначала создать и инициализировать переменную, а затем наполнить ее. Пример обращения к корневой сущности через $link() можно найти в следующем фрагменте кода:
// Для продуктов запрашиваем сервисы, а для сервисов операции, имена которых начинаются с имени продукта.
ProductPartyCollectionWith<productgrasp> ppWith = GraphCreator.selectProductParty();
ppWith
.withCode()
.withPerformedServices(servGraph -> servGraph
.withCode()
.withPerformedOperations(opGraph -> opGraph
.withAmount()
// обращение к корневому элементу через переменную и метод $link()
.setWhere(opWhere -> opWhere.nameLike(ppWith.$link().name().concat("%")))
)
)
Примечание
Через метод
$link()можно обращаться только к вышестоящим сущностям. Обратиться к атрибутам вложенных сущностей можно через ссылки на объекте.
Пример использования метода $link() приведен в классе SearchDemoTests (из проекта dataspace-client), метод searchWithHierarchyTest.
Внимание!
В подобных конструкциях нельзя использовать замыкания вышестоящих Grasp-объектов, т.к. это приведет к построению некорректного условия фильтрации. Подобные ошибки не будут выявлены на этапе компиляции и могут привести к неправильному (но успешному) результату поиска. Таким образом, не следует использовать для передачи атрибута сущности вышестоящий Grasp-объект.
Ограничение поля по коллекции значений#
Пример ограничения поля по коллекции значений можно найти в следующем фрагменте кода:
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.codeIn("code1", "code2", "code3");
// или
List<string> codeList = Arrays.asList("code1", "code2", "code3");
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.codeIn(codeList);
Ограничение поля по коллекции примитивов#
Пример ограничения поля по коллекции примитивов можно найти в следующем фрагменте кода:
GraphCreator.selectProductParty()
.withCode()
// Условие, по сути, является синонимом условия ppWhere.statesContains(ppWhere.code())
.setWhere(ppWhere -> ppWhere.codeIn(ppWhere.states()));
Объединение условий с помощью конструкций «and» и «or»#
Пример объединения условий с помощью конструкций and и or:
// ниже описано следующее условие: (code == "code1" or code == "code2") and (name == "name1" or name == "name2")
// вызов операции .and() или .or() как бы обрамляет все левое условие в скобки
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.codeEq("code1").or(ppWhere.codeEq("code2")).and(ppWhere.nameEq("name1").or(ppWhere.nameEq("name2")));
// это же условие можно записать при помощи статических операций and и or
import static com.sbt.pprb.ac.grasp.base.BaseGrasp.and;
import static com.sbt.pprb.ac.grasp.base.BaseGrasp.or;
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere ->
and(
or(
ppWhere.codeEq("code1"),
ppWhere.codeEq("code2"),
),
or(
ppWhere.nameEq("name1"),
ppWhere.nameEq("name2"),
)
)
);
Построение условий в зависимости от входных условий (параметров)#
Пример использования входных условий можно найти в следующем фрагменте кода:
GraphCreator.selectProduct()
.withCode()
.withName()
.withBeginDate()
.setWhere(where -> {
// Формируем условие поиска в зависимости от заданных условий
AndContainer andContainer = GraspHelper.emptyAnd();
// code EQ value
if (testCode0 != null) {
andContainer.put(where.codeEq(testCode0));
}
// AND name EQ value
if (testName0 != null) {
andContainer.put(where.nameEq(testName0));
}
// (beginDate NOT EQ value) ...
OrContainer orContainer = GraspHelper.emptyOr();
if (testBeginDate != null) {
orContainer.put(where.beginDateNotEq(testBeginDate));
}
// ... OR (endDate NOT EQ value))
if (endDate0 != null) {
orContainer.put(where.endDateNotEq(endDate0));
}
NotContainer notContainer = GraspHelper.emptyNot();
// NOT ((beginDate NOT EQ value) OR (endDate NOT EQ value))
notContainer.set(orContainer);
andContainer.put(notContainer);
return andContainer.create();
});
Ограничение по атрибутам вложенной сущности#
Пример использования атрибутов вложенной сущности можно найти в следующем фрагменте кода:
// Изменена искомая сущность на PerformedService, т.к. у этой сущности имеется ссылка на ProductParty с названием атрибута "product".
GraphCreator.selectPerformedService()
.withCode()
.setWhere(psWhere -> psWhere.product().codeLike("codePrefix%"));
Уточнение типа вложенного объекта или коллекции объекта в ограничивающем условии#
Пример уточнения типа вложенного объекта и коллекции можно найти в следующем фрагменте кода:
GraphCreator.selectPerformedService()
.withCode()
// уточнение типа у вложенного объекта
.setWhere(psWhere -> psWhere.product(picker -> picker.Deposit(),
pWhere -> pWhere.sumGreater(new BigDecimal(10))
// уточнение типа у коллекции вложенных объектов
.and(depositGrasp.performedOperationsContains(picker -> picker.CreditOperation(),
opWhere -> opWhere.currencyEq(Currency.RUB)))
)
);
Ограничения по SoftReference и ComplexReference полям#
Пример использования ограничений по полям SoftReference и ComplexReference можно найти в следующем фрагменте кода:
GraphCreator.selectProduct()
.setWhere(where ->
// owner (Client) — SoftReference, в котором есть только один идентификатор — entityId
where.ownerEq(clientId)
// initialService — ComplexReference, в котором есть как entityId, так и rootEntityId
.and(where.performedOperationsContains(CreditOperationGrasp.class, po ->
po.initialServiceEq("serviceId", "aggreateId"))));
// Альтернативный способ (менее лаконичный, но предоставляет большие возможности)
GraphCreator.selectProduct()
.setWhere(where ->
// owner (Client) — SoftReference, в котором есть только один идентификатор — entityId
where.owner().entityIdEq(clientId)
// initialService — ComplexReference, в котором есть как entityId, так и rootEntityId
// расширенный способ позволяет производить не только Eq операции
.and(where.performedOperationsContains(CreditOperationGrasp.class, po ->
po.initialService().entityIdLike("%2").and(po.initialService().rootEntityIdEq(initServiceOwnerId)))));
Ограничение по вложенной коллекции (exists)#
С помощью DataSpace можно делать ограничения выборки по вложенной коллекции аналогично SQL-команде Exists. Пример ограничения выборки по вложенной коллекции можно найти в следующем фрагменте кода:
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.performedOperationsContains(poWhere -> poWhere.codeLike("codePrefix%")));
Вложенная коллекция объявляется на модели явно.
Ограничение по несвязанной коллекции (exists/not exists)#
В запросе выборки можно связать две коллекции, которые не связаны по модели между собой.
Примером выборки несвязанной коллекции может служить следующий фрагмент кода:
PerformedServiceCollectionWith performedServiceCollectionWith1 = PerformedServiceGraph.createCollection()
.setWhere(where -> EntitiesCollections.RequestInstExists( param -> param.nameEq(where.name()) ));
Где данный блок кода позволяет получить значение тех performedService, имя которых присутствует в таблице RequestInst в поле name.
Применение агрегирующих функций в условиях фильтрации#
В DataSpace доступны агрегирующие функции: count, min, max, avg. Эти агрегирующие функции применимы к свойствам-коллекциям. После применения агрегирующих функций необходимо через точку задать ограничивающее условие: eq, notEq, greater, lower и др.
Примеры использования агрегирующих функций можно найти во фрагменте кода:
// count без условия фильтрации
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.performedOperationsCount().eq(5));
// count с условием фильтрации
GraphCreator.selectProductParty ()
.withCode()
.setWhere(ppWhere -> ppWhere.performedOperationsCount(poWhere -> poWhere.codeLike("codePrefix%")).eq(2));
// Min без условия фильтрации — в качестве аргумента функции задается поле сущности по которому выполняется агрегатная функция. Можно задать только одно поле!
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.performedOperationsMin(operation -> operation.exchangeRate()).eq(BigDecimal.ONE));
// Min с условием фильтрации — в качестве первого аргумента функции задается поле сущности по которому выполняется агрегатная функция. Можно задать только одно поле!
// В качестве второго аргумента функции задается условие фильтрации коллекции
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.performedOperationsMin(operation -> operation.exchangeRate(), poWhere -> poWhere.codeLike("codePrefix%")).eq(BigDecimal.ONE));
// Max без условия фильтрации — в качестве аргумента функции задается поле сущности по которому выполняется агрегатная функция. Можно задать только одно поле!
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.performedOperationsMax(operation -> operation.exchangeRate()).eq(BigDecimal.ONE));
// Max с условием фильтрации — в качестве первого аргумента функции задается поле сущности по которому выполняется агрегатная функция. Можно задать только одно поле!
// В качестве второго аргумента функции задается условие фильтрации коллекции
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.performedOperationsMax(operation -> operation.exchangeRate(), poWhere -> poWhere.codeLike("codePrefix%")).eq(BigDecimal.ONE));
// Sum без условия фильтрации — в качестве аргумента функции задается поле сущности по которому выполняется агрегатная функция. Можно задать только одно поле!
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.performedOperationsSum(operation -> operation.exchangeRate()).eq(BigDecimal.ONE));
// Sum с условием фильтрации — в качестве первого аргумента функции задается поле сущности по которому выполняется агрегатная функция. Можно задать только одно поле!
// В качестве второго аргумента функции задается условие фильтрации коллекции
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.performedOperationsSum(operation -> operation.exchangeRate(), poWhere -> poWhere.codeLike("codePrefix%")).eq(BigDecimal.ONE));
// Avg без условия фильтрации — в качестве аргумента функции задается поле сущности по которому выполняется агрегатная функция. Можно задать только одно поле!
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.performedOperationsAvg(operation -> operation.exchangeRate()).eq(BigDecimal.ONE));
// Avg с условием фильтрации — в качестве первого аргумента функции задается поле сущности по которому выполняется агрегатная функция. Можно задать только одно поле!
// В качестве второго аргумента функции задается условие фильтрации коллекции
GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.performedOperationsAvg(operation -> operation.exchangeRate(), poWhere -> poWhere.codeLike("codePrefix%")).eq(BigDecimal.ONE));
Запрос значения поля из несвязанного по модели класса через $withCalculated()#
С помощью метода $withCalculated() можно выбирать не только поля из текущего класса, но и применять агрегирующие функции на любом классе модели.
Пример:
AclassCollectionWith<? extends AclassGrasp> req = AclassGraph.createCollection()
.withCode()
.$withCalculated("f1", ff ->
EntitiesCollections.BclassMax(field -> field.objectId(), reqWhere -> reqWhere.aLink().entityId().eq(ff.id())));
Чтение запрошенного значения:
GraphCollection<AclassGet> res = searchClient.searchAclass(req);
String f1 = res.get(0).$getCalculated("f1", String.class);
Операции с числовыми полями#
В DataSpace предусмотрена возможность выполнять математические операции сложения, вычитания, умножения и деления. Эти операции доступны для всех числовых полей. В качестве числовых значений также могут быть представлены другие числовые поля или выражения.
Если требуется использовать в выражении число, и оно должно быть первым аргументом в выражении, то необходимо применить статический метод valueOf(Number n) класса GraspHelper (другие методы данного класса приводятся ниже).
Вспомогательные функции (например, округление) выделены в отдельном классе GraspHelper.
Примеры задания предикатов с числовыми операциями можно найти в следующем фрагменте кода:
// сложение
GraphCreator.selectPerformedOperation()
.setWhere(poWhere -> poWhere.exchangeRateEq(poWhere.referenceOrder().plus(9999.3)));
GraphCreator.selectPerformedOperation()
.setWhere(poWhere -> poWhere.exchangeRateEq(valueOf(9999.3).plus(poWhere.referenceOrder()));
// вычитание
GraphCreator.selectPerformedOperation()
.setWhere(poWhere -> poWhere.exchangeRateGreater(poWhere.referenceOrder().minus(100)));
GraphCreator.selectPerformedOperation()
.setWhere(poWhere -> poWhere.exchangeRateGreater(valueOf(100).minus(poWhere.referenceOrder()));
// умножение
GraphCreator.selectPerformedOperation()
.setWhere(poWhere -> poWhere.exchangeRateLess(poWhere.referenceOrder().mul(10)));
GraphCreator.selectPerformedOperation()
.setWhere(poWhere -> poWhere.exchangeRateLess(valueOf(10).mul(poWhere.referenceOrder())));
// деление
GraphCreator.selectPerformedOperation()
.setWhere(poWhere -> poWhere.exchangeRateGreater(poWhere.referenceOrder().div(2)));
GraphCreator.selectPerformedOperation()
.setWhere(poWhere -> poWhere.exchangeRateGreater(valueOf(2).div(poWhere.referenceOrder())));
// округление математическое
GraphCreator.selectPerformedOperation()
.setWhere(poWhere -> GraspHelper.round(where.summaOperation()).greater(50));
// округление в большую сторону
GraphCreator.selectPerformedOperation()
.setWhere(poWhere -> GraspHelper.ceil(where.summaOperation()).eq(51));
// округление в меньшую сторону
GraphCreator.selectPerformedOperation()
.setWhere(poWhere -> GraspHelper.floor(where.summaOperation()).eq(50));
Операции с полями типа «Дата» (+»Время»)#
Для полей с типом «Дата» предусмотрены следующие арифметические функции:
addSeconds()— добавление (уменьшение) секунд;addMinutes()— добавление (уменьшение) минут;addHours()— добавление (уменьшение) часов;addDays()— добавление (уменьшение) дней;addMonths()— добавление (уменьшение) месяцев;addYears()— добавление (уменьшение) лет.
Примеры использования временных арифметических функций можно найти во фрагменте ниже:
PerformedOperationGraph.createCollection()
// Метод addDays добавляет дни, addHours — часы, и т.п.
// Методы можно применять последовательно
// В данном примере проверяем, что между endDate и beginDate больше 3.5 дней
// endDate — beginDate > 3.5 дней => endDate > beginDate + 3.5 дня
.setWhere(where -> where.endDateGreater(where.beginDate().addDays(3).addHours(12)));
// добавление к дате миллисекунд осуществляется как добавление к дате секунд * 0,001
// в примере к дате добавляется 27 миллисекунд
PerformedOperationGraph.createCollection()
.setWhere(where -> where.endDateGreater(where.beginDate().addSeconds(0.027)));
Ограничения по несвязанным сущностям#
В условиях поиска можно использовать данные, которые хранятся в несвязанных таблицах.
Для построения запроса используется класс EntitiesCollections. Внутри содержатся методы, название которых состоит из имени класса и агрегирующей функции (min, max, avg, count).
Пример:
GraphCreator.selectPerformedOperation()
.withCode()
// Условие означает сравнение поля из конкретной сущности с максимальным значением поля среди всех сущностей класса ProductParty
.setWhere(where -> where.endDateGreater(EntitiesCollections.ProductPartyMax(fieldSelection -> fieldSelection.endFactDate())));
Множество сущностей, на которые применяется агрегирующая функция, можно ограничить:
GraphCreator.selectPerformedOperation()
.withCode()
// Для методов с агрегирующими функциями существуют перегруженные, которые дополнительно принимают условие фильтрации
.setWhere(where -> where.endDateGreater(EntitiesCollections.ProductPartyMax(fieldSelection -> fieldSelection.endFactDate(), filter -> filter.codeLike("test%"))));
Можно построить часть поискового условия с методом Exists на другую сущность:
GraphCreator.selectPerformedOperation()
.withCode()
// Одна из частей условия — проверка на существование хотя бы одной сущности, удовлетворяющей условию
.setWhere(where -> where.codeEq("123").and(EntitiesCollections.ProductPartyExists(ppGrasp -> ppGrasp.beginDateIsNotNull())));
Агрегатная функция First#
Данная функция позволяет в условиях фильтрации использовать первый элемент некоторой коллекции. При применении данной функции настоятельно рекомендуется указывать направление и критерии сортировки коллекции.
Ниже приведен пример получения списка операций по некоторому сервису, стоимость которых превышает стоимость последней операции по этому сервису.
Примечание
На текущий момент метод
FIRSTнедоступен через классEntitiesCollectionsпо причине значительного увеличения количества методов в данном классе с ростом числа сущностей модели. Планируется добавление данного метода после рефакторинга методов в данном классе по Feature Toggling.Для применения метода необходимо использовать конструкцию вида
where.getEntitiesCollections().Operation(...), гдe:
where— Grasp-объект;
Operation— имя сущности, к которой будет применен методFIRST.
Пример применения метода на коллекции несвязанных сущностей:
ServiceCollectionWith<? extends ServiceGrasp> req = ServiceGraph.createCollection()
.withCode()
.withOperations(op -> op
.withCode()
.withCost()
.withOpDate()
.setWhere(where -> where.cost().greater(
where.getEntitiesCollections().Operation(
AggregateFunction.FIRST, // Тип агрегатной функции
OperationGraspCollection::cost, // Выбираемое свойство сущности
w -> w.service().id().eq(where.service().id()), // условие фильтрации (может быть null)
sort -> sort.desc(OperationGrasp::opDate), // условие сортировки
null, // limit, для агрегатной функции FIRST не применим
null // offset
)
))
)
.setWhere(where -> where.id().eq(someServiceId));
Примечание
Параметры
limitиoffsetприменяются до применения агрегатных функций. При применении агрегатной функцииFIRST, параметрlimitвне зависимости от значения заменяется значением1L.На БД H2 корректная работа параметров
limitиoffsetне гарантируется — вызвано ограничениями работы БД H2 с подзапросами.
Сортировка объектов#
Параметры сортировки устанавливаются при помощи метода setSortingAdvanced(). Этот метод обеспечивает возможность задавать сложные условия сортировки и защищенный вызов методов.
Примечание
Также существует устаревший метод
setSorting(). Данный метод не обеспечивает защищенность вызова и допускает указание в качестве критерия сортировки только примитивных полей корневой сущности поиска.Метод
setSorting()не рекомендован к использованию.
Пример сортировки объектов можно найти в следующем фрагменте кода:
GraphCreator.selectProductParty()
.setSortingAdvanced(sortBuilder ->
sortBuilder.asc(grasp -> grasp.objectId())
.desc(grasp -> grasp.code())
);
Пример сортировки объектов по атрибутам вложенного объекта показан в следующем фрагменте кода:
// Запрашиваем сервисы с сортировкой по коду продукта, к которому они привязаны
GraphCreator.selectPerformedService()
.setSortingAdvanced(sort -> sort.desc(f -> f.product().code()))
);
Пример сортировки объектов по атрибутам вложенного объекта показан в следующем фрагменте кода. Для уточнения типа вложенного объекта используется метод с постфиксом «As» (в примере productAs):
GraphCreator.selectPerformedService()
.setSortingAdvanced(sort -> sort.desc(f -> f.productAs(picker -> picker.Deposit()).declaration()));
Для сортировки по коллекции обязательно необходимо использовать одну из агрегирующих функций. Более подробные сведения можно найти в разделе «Применение агрегирующих функций в условиях фильтрации».
Пример сортировки объектов по коллекции (с использованием агрегатной функции) можно найти в следующем фрагменте кода.
// Сортировка сервисов производится по минимальному значению поля amount среди всех операций каждого сервиса
GraphCreator.selectPerformedService()
.setSortingAdvanced(sort -> sort.desc(f -> f.performedOperationsMin(pof -> pof.amount())))
.setWhere(where -> where.codeLike(prefix + "%"))
);
Вспомогательный класс GraspHelper#
Класс GraspHelper предоставляет вспомогательные функции, применяемые при формировании поискового условия.
Список доступных функций может расширяться с развитием SDK.
Вспомогательные функции базируются на следующих классах:
ConditionWrapper— класс, содержащий поисковый предикат;AndContainer— контейнер, объединяющий помещаемые в него поисковые предикаты через AND;OrContainer— контейнер, объединяющий помещаемые в него поисковые предикаты через OR;NotContainer— контейнер, инвертирующий поисковый предикат;BaseNumericField— базовый класс, представляющий числовые атрибуты объектов;NumericField— класс, представляющий числовой атрибут объекта;StringField— класс, представляющий строковый атрибут объекта.
Доступные через GraspHelper вспомогательные классы и методы показаны в таблице:
КЛАСС |
ОПИСАНИЕ ФУНКЦИИ |
ПРИМЕР КОДА |
|---|---|---|
|
Инвертирует поисковой предикат (добавляет |
|
|
Создает пустой контейнер, объединяющий добавляемые в него предикаты через «and». Используется при динамическом формировании условий фильтрации |
|
|
Создает пустой контейнер, объединяющий добавляемые в него предикаты через OR. Используется при динамическом формировании условий фильтрации |
|
|
Создает пустой контейнер, инвертирующий добавляемый в него предикат. Используется при динамическом формировании условий фильтрации |
|
|
Переводит число в объект класса |
|
|
Переводит строку в объект класса |
|
|
Переводит строку или атрибут объекта, представленные классом |
|
|
Возвращает округленное число или атрибут объекта, представленные классом |
|
|
Возвращает округленное в большую сторону число или атрибут объекта, представленные классом |
|
|
Возвращает округленное в меньшую сторону число или атрибут объекта, представленные классом |
|
|
Преобразует строку к классу |
|
|
Переводит строку или атрибут объекта, представленные классом |
|
|
Переводит строку или атрибут объекта, представленные классом |
|
|
Переводит строку или атрибут объекта, представленные классом |
|
|
Удаляет пробельные символы слева у строки или атрибута объекта, представленного классом |
|
|
Удаляет пробельные символы справа у строки или атрибута объекта, представленного классом |
|
|
Возвращает подстроку строки или атрибута объекта, представленного классом |
|
|
Возвращает подстроку строки или атрибута объекта, представленного классом |
|
|
Возвращает подстроку строки или атрибута объекта, представленного классом |
|
|
Возвращает подстроку строки или атрибута объекта, представленного классом |
|
|
Осуществляет замену подстроки в строке или атрибуте объекта, представленного классом |
|
|
Осуществляет замену подстроки в строке или атрибуте объекта, представленного классом |
|
|
Возвращает длину строки или длину строкового атрибута объекта, представленного классом |
|
|
Объединяет переданные строки или атрибуты объекта, представленные классом |
|
|
Возвращает первое ненулевое (null) значение из списка |
`public static NumericField coalesce(NumericField field, NumericField<?>… fields) { return coalesceNumbers(field, fields); } |
Пример использования вспомогательной функций из GraspHelper:
GraphCreator.selectProductParty()
.withCode()
// Переводим код в верхний регистр и сравниваем с "UPPER_CODE"
.setWhere(ppWhere -> GraspHelper.upper(ppWhere.code()).eq("UPPER_CODE"));
// Пример с coalesce()
GraphCollection<ProductGet> collection = dataspaceCoreSearchClient.searchProduct(pcw -> pcw
.withCode()
.withName()
.setWhere(where -> GraspHelper.coalesce(where.description(), where.code(), where.name()).eq(testCode0)));
Способы уточнения типов сущностей (Picker)#
Существуют методы, которые позволяют уточнить тип запрашиваемой или фильтруемой сущности. К числу таких методов относятся, например, методы запроса вложенных сущностей и коллекций вложенных сущностей, а также методы фильтрации. Для этого первым параметром передается информация о классе-потомке сущности. Предусмотрены следующие способы передачи такой информации:
Передать в качестве первого параметра целевой класс.
Использовать один из методов класса
Picker.
Класс Picker содержит методы, возвращающие классы-потомки заданного класса.
Недостаток первого подхода — возможность указать неправильный класс. Это может привести к трудно отлаживаемым ошибкам, т.к. среда разработки начинает подсвечивать не параметр-класс, а следующую за параметром лямбду.
Класс Picker позволяет избежать подобного рода ошибок, упрощает процесс выбора класса потомка (нет необходимости вспоминать названия), устраняет необходимость написания полного имени класса вместе с дженериками.
В примере ниже показано, каким образом можно уточнить тип сущности через указание класса сущности:
GraphCreator.selectPerformedService()
.withProduct(DepositCollectionWith.class, dep -> dep.withSum());
//или
GraphCreator.selectPerformedService()
.withProduct(DepositCollectionWith.CLS, dep -> dep.withSum());
В примере ниже показано, каким образом можно уточнить тип сущности через класс Picker:
GraphCreator.selectPerformedService()
.withProduct(picker -> picker.Deposit(), dep -> dep.withSum());
//или
GraphCreator.selectPerformedService()
.withProduct(ProductCollectionWithPicker::Deposit, dep -> dep.withSum());
Чтобы picker отобразил варианты выбора при редактировании кода, необходимо сначала ввести запятую, и только после этого вводить точку после объекта picker. Иначе среда разработки посчитает, что задается лямбда выбора полей, а не классов:

Доступные для выборки и фильтрации системные свойства#
Имеется ряд системных полей (автоматически генерируемых), которые доступны для выборки и фильтрации по ним. К таким полям относится lastChangeDate. Свойство хранит время последней модификации объекта.
Наложение условий в зависимости от типа объекта коллекции#
При запросе коллекции некоторых сущностей (например, ProductParty) в результат попадают не только «чистые» ProductParty, но и его потомки. В некоторых ситуациях при запросе ProductParty может потребоваться наложить дополнительные условия, если объект будет являться одним из потомков запрошенного типа.
Пример наложения таких условий можно найти в следующем фрагменте:
GraphCreator.selectProductParty()
.withCode()
// Если сущность является Deposit, то проверяем по minSum
// Если сущность — обычный Product, то сравниваем по code
.setWhere(Arrays.asList(
GraspHelper.wrapCondition(DepositGrasp.class, grasp -> grasp.minBalanceEq(BigDecimal.valueOf(10))),
GraspHelper.wrapCondition(ProductGrasp.class, grasp -> grasp.codeEq(testCode2))))
// Расширяем спецификацию для того, чтобы получить у него поле minSum, если класс найденного объекта будет Deposit
.extend(ProductWithPicker::Deposit, DepositWith::withMinBalance);
Передача запроса на сервер и получение ответа#
Запросы на сервер передаются при помощи экземпляра класса DataspaceCoreSearchClient, как показано в примере ниже. Конструктор класса принимает единственный параметр — URI серверной части.
DataspaceCoreSearchClient searchClient = new DataspaceCoreSearchClient("http://192.168.0.1:8080");
Рекомендуется использовать один экземпляр DataspaceCoreSearchClient на проект.
Запрос на сервер можно передать двумя способами:
Способ 1. Заранее подготовить поисковой запрос, сохранив его в переменной с типом <имя сущности> CollectionWith. После чего при необходимости вызвать соответствующий типу сущности метод поиска из
DataspaceCoreSearchClient. Методы поиска имеют названия, соответствующие следующему шаблону: search<имя сущности>. В результате поиска возвращается коллекция типаGraphCollection, параметризированная интерфейсом <имя искомой сущности> Get. Пример использования предварительно подготовленного запроса показан в следующем фрагменте:ProducrtPartyCollectionWith ppWith = GraphCreator.selectProductParty() .withCode() .withBeginDate() .setWhere(ppWhere -> ppWhere.codeLike("codePrefix%")); GraphCollection<ProductPartyGet> result = searchClient.searchProductParty(ppWith);Этот способ позволяет дополнить или изменить запрос до его непосредственного вызова, например, изменить параметры постраничной выборки (
limit,offset).Способ 2. Сделать описание поискового запроса в момент его вызова на
searchClient. Пример использования предварительно подготовленного запроса показан во фрагменте кода:GraphCollection<productpartyget> result = searchClient.searchProductParty(ppWith -> .withCode() .withBeginDate() .setWhere(ppWhere -> ppWhere.codeLike("codePrefix%")) );
Ответ с сервера преобразуется к объекту класса GraphCollection<<имя искомой сущности>Get>, параметризированного Get-интерфейсом искомой сущности.
Преобразование в результат ленивое в отношении ссылочных полей и полей-коллекций. Данные поля заполняются в момент вызова соответствующего get-метода.
В GraphCollection предусмотрены следующие основные методы:
get(int)— получение элемента по его порядковому номеру (нумерация с 0).getTotalCount()— получение общего количества элементов, удовлетворяющих условию фильтрации, если при запросе был установлен флаг через методsetTotalCount(true).getCollection()— возвращает лежащую в основеGraphCollectionколлекцию (может быть List или Set). Если передать в качестве параметраGet-интерфейс, то коллекция будет отфильтрована по этому интерфейсу и будут возвращены только объекты указанного интерфейса. Это позволяет получить отдельные элементы при запросе с детализацией сущности.isEmpty()— возвращает «true», если коллекция пустая. ЗначениеtotalCountне влияет на результат данного метода.iterator()— возвращает итератор по коллекции.size()— возвращает количество полученных элементов (элементов в коллекции).stream()— превращает коллекцию в поток.
Получить запрошенные значения можно через соответствующие полям get-методы.
Получение запрошенных с сервера данных:
GraphCollection<performedserviceget> result = searchClient.searchPerformedService(psWith ->
.withCode()
.withBeginDate()
.withProduct(ProductPartyWithPicker::DepositCBExmpl, prodWith -> prodWith.withCode().withName())
.withPerformedOperations(poWith -> poWith.withCode().withName().withBeginDate())
.setWhere(ppWhere -> ppWhere.codeLike("codePrefix%"))
.extend(ServicePlusWith.class, g -> g.withSpField())
.extend(ServicePlus2With.class, g -> g.withSp2Field())
);
// Получение примитивных атрибутов корневой сущности
PerformedServiceGet psGet = result.get(0);
String psCode = psGet.getCode();
Date psBeginDate = psGet.getBeginDate();
// Получение вложенного объекта и его атрибутов
ProductPartyGet ppGet = psGet.getProduct();
String ppCode = ppGet.getCode();
String ppName = ppGet.getName();
// Получение вложенного объекта с уточнением типа
DepositCBExmpl depGet = psGet.getProduct(ProductPartyGetPicker::DepositCBExmpl);
String declaration = depGet.getDeclaration(); // свойство класса DepositCBExmpl
// Получение коллекции вложенных объектов и их атрибутов
GraphCollection<performedoperationget> poResult = psGet.getPerfromedOperations();
PerformedOperationGet poGet = poResult.get(0);
String poCode = poGet.getCode();
// Получение корневых объектов заданного типа
List<serviceplus2get> servicePlus2Collection = result.getCollection(ServicePlus2Get.class);
Виды генерации класса DataspaceCoreSearchClient#
Так как класс DataspaceCoreSearchClient является генерируемым, возможна ситуация, когда он содержит большое количество методов. Это затрудняет разработку, или при большом количестве классов в модели JDK не может его скомпилировать. Для решения данной проблемы можно воспользоваться настройкой в конфигурации плагина model-api-generator-maven-plugin:
<groupClientMethods>true</groupClientMethods>
После указания значения «true» данной настройки в классе DataspaceCoreSearchClient перестают генерироваться методы поиска.
Вместо этого для каждого класса модели создается одноименное поле, которое содержит методы поиска и получения объектов этого класса.
Например, при включенной настройке так будет выглядеть вызов поиска объектов класса ProductParty:
searchClient.productParty.search(ppWith ->
.withCode()
.withBeginDate()
.setWhere(ppWhere -> ppWhere.codeLike("codePrefix%"))
);
Настройка groupClientMethods по умолчанию имеет значение «false», в этом случае все поисковые методы находятся в одном классе.
В таблице на примере класса ProductParty показаны различия в вызове некоторых поисковых методов при разных значениях настройки:
false |
true |
|---|---|
searchProductParty |
productParty.search |
getProductParty |
productParty.get |
searchCountProductParty |
productParty.searchCount |
searchCountProductPartyAsync |
productParty.searchCountAsync |
Объединение запросов к разным агрегатам, обладающим одним интерфейсом (merge)#
Рассмотрим данную функциональность на примере. Предположим, необходимо вести события. При этом события могут быть двух видов:
общие для всех организаций;
относящиеся только к одной конкретной организации.
События, относящиеся к определенной организации, будут привязаны к агрегату организации. Общие события могут сами быть агрегатами или же входить в другой агрегат.

Необходимо вывести для заданной организации все события (и собственные, и общие) одним списком, упорядоченным по дате начала события с пагинацией.
Для решения задачи необходимо, чтобы у объединяемых в рамках запроса сущностей совпадали имена и размерности полей, по которым будет осуществляться сортировка.
Общие поля сущностей выделяют в интерфейс, который наследуют сущности. Файл model.xml может иметь следующий вид:
<interface name="Event">
<property name="code" type="String"/>
<property name="name" type="String"/>
<property name="beginDate" type="Date"/>
<property name="endDate" type="Date"/>
</interface>
<class implements="Event" label="Общее событие" name="CommonEvent">
<property label="Код события" name="code" type="String"/>
<property label="Название события" name="name" type="String"/>
<property label="Дата начала события" name="beginDate" type="Date"/>
<property label="Дата окончания события" name="endDate" type="Date"/>
<property label="Тип события" name="eventType" type="String"/> <!-- для краткости применен тип String, а не справочник -->
</class>
<class label="Организация" name="Organization">
<property label="Название организации" name="name" type="String"/>
<property mappedby="orgId" name="ogrEvents" type="OrgEvent"/>
</class>
<class implements="Event" label="Событие организации" name="OrgEvent">
<property label="Код события" name="code" type="String"/>
<property label="Название события" name="name" type="String"/>
<property label="Дата начала события" name="beginDate" type="Date"/>
<property label="Дата окончания события" name="endDate" type="Date"/>
<property label="Организация, которой принадлежит событие" name="orgId" parent="true" type="Organization"/>
<property label="доп. код события" name="subCode" type="String"/>
</class>
Следует отметить, что в описании соответствующих классов появился атрибут implements="Event", обозначающий, что класс имплементирует указанный интерфейс. При имплементации нескольких интерфейсов их имена указываются через запятую, допускаются пробелы (например, implements=»Event, Codeable»). При этом при построении объединенного запроса можно использовать только один из интерфейсов, невозможно использовать в объединении запросов сразу два и более интерфейса.
Ниже представлен пример объединения запросов при помощи интерфейса. Полагаем, что все необходимые для запроса данные в БД уже есть. Следует отметить, что в отдельных запросах можно указывать специфичные для типов поля, а не только те поля, что указаны в интерфейсе. Фильтрация данных осуществляется в каждом их объединяемых запросов в отдельности. Но сортировка данных осуществляется уже через интерфейс (сортировку можно производить только по полям, описанным в интерфейсе). При объединении запросов сортировка и пагинация внутри них не допускается, только на уровне объединения запросов.
В примере ниже показано, каким образом интерфейс можно использовать для объединения запросов:
// Формируем запрос на выборку CommonEvent
CommonEventCollectionWith<CommonEventGrasp> commonEventCollectionWith
= GraphCreator.selectCommonEvent()
.withCode()
.withName()
.withBeginDate()
.withEndDate()
// запрашиваем специфичные для CommonEvent поля
.withEventType()
// ограничиваем доставаемые из БД данные
.setWhere(where -> where.beginDateBetween(dFrom, dTo));
// Формируем запрос на выборку OrgEvent
OrgEventCollectionWith<OrgEventGrasp> orgEventCollectionWith
= GraphCreator.selectOrgEvent()
.withCode()
.withName()
.withBeginDate()
.withEndDate()
// запрашиваем специфичные для OrgEvent поля
.withSubCode()
// ограничиваем доставаемые из БД данные
.setWhere(where -> where.beginDateBetween(dFrom, dTo));
// Объединяем два запроса в одну выборку с общей сортировкой по beginDate и code
// Строим запрос по интерфейсу
EventMerge<EventGrasp> eventMerge = GraphCreator.selectEvent()
// в функцию merge передаем объединяемые запросы (их может быть больше двух)
.merge(commonEventCollectionWith, orgEventCollectionWith)
// настраиваем пагинацию
.setOffset(0)
.setLimit(10)
// запрашиваем общее количество данных в БД
.setTotalCount(true)
// настраиваем сортировку по атрибутам интерфейса
.setSortingAdvanced(sort -> sort
.asc(fieldPicker -> fieldPicker.beginDate())
.asc(fieldPicker -> fieldPicker.code())
);
// непосредственное исполнение объединенных запросов
GraphCollection<EventGet> events = dataspaceCoreSearchClient().searchEvent(eventMerge);
// обход полученных данных
events.forEach(event -> {
// вычитываем общие поля
String code = event.getCode();
// вычитываем специфичные для типа CommonEvent поля
if (event instanceof CommonEventGet) {
String type = ((CommonEventGet) event).getEventType();
//Do something
}
// вычитываем специфичные для типа OrgEvent поля
if (event instanceof OrgEventGet) {
String subCode = ((OrgEventGet) event).getSubCode();
//Do something
}
//Do something
});
// вместо поэлементного обхода результата можно получить все элементы заданного типа следующим образом:
Collection<OrgEventGet> orgEvents = events.getCollection(OrgEventGet.class);
Примечание
В интерфейс можно выносить не только поля с примитивными типами, но и поля, имеющие объектовый тип или поля коллекции.
Все классы, имплементирующие интерфейс, должны сохранять название и тип атрибутов из интерфейса (нельзя изменить название атрибута при имплементации интерфейса).
Разыменование внешних ссылок в рамках одного шарда#
Агрегатоцентричная модель DataSpace предполагает разделение модели потребителя по непересекающимся агрегатам. Связь элементов, принадлежащих разным агрегатам, осуществляется через так называемые «внешние ссылки». При этом предполагается, что агрегаты (со всеми своими данными) могут перейти в другой шард. Но если имеется уверенность, что данные по внешней ссылке находятся в том же шарде, то имеется возможность разыменования такой ссылки.
Внимание!
Разыменование внешних ссылок осуществляется в том же шарде, откуда осуществляется запрос. При использовании данной функциональности потребитель осознает и принимает ответственность за то, что, если данные другого агрегата мигрируют в другой шард, то результат поиска может измениться (данные не будут найдены).
Пример того, каким образом внешние ссылки можно разыменовать, показан в следующем фрагменте кода:
// Предположим, есть сущность Request, у которой имеется внешняя ссылка на Product с именем product
// Запрос с разыменованием этой внешней ссылки
GraphCreator.selectRequest()
.withCode()
// Перегруженный метод принимает лямбду, в которой можно задавать спецификацию для разыменованной ссылки
.withProduct(productWithLinkable -> productWithLinkable.withCode().withName())
// В ограничивающем условии на объекте с внешней ссылкой появляется "виртуальное" (в плане отсутствия в БД) поле с именем entity,
// по которому можно построить условие
.setWhere(where -> where.product().entity().codeEq("someCode"));
Чтение истории изменения статусов#
С помощью метки перехода можно определить, был ли переход сущности в новый статус прямым или осуществился возврат к ранее выставленному статусу. Рассмотрим чтение истории статусов на следующей модели:
<model>
<class name="ProductParty">
<property name="code" type="String" label="код" index="true" historical="true"/>
</class>>
</model>
...
<status-classes class="ProductParty">
<stakeholder code="platform" name="наблюдатель по ЖЦ объекта"/>
</status-classes>
<statuses class="ProductParty" historical="true">
<stakeholder-link code="platform">
<status code="productCreated" description="Начальный статус продукта" name="Создание продукта"
initial="true">
<to status="productClosed" label="forward"/>
<to status="productCheck" label="forward"/>
</status>
<status code="productClosed" name="На закрытие"/>
<status code="productCheck" name="На рассмотрении">
<to status="productClosed" label="forward"/>
<to status="productCreated" label="backward"/>
</status>
</stakeholder-link>
</statuses>
Граф перехода дополняется атрибутом label с типом UnicodeString длинной в 254 символа. При обработке модели осуществляется контроль значения длины данного атрибута.
Значение данного атрибута берется из описания графа перехода на модели.
В базе данных хранится только последнее значение состояния перехода.
Изменения метки допустимы только в новых версиях модели.
Сущность истории изменения статусов дополняется атрибутом transition с типом идентификатора StatusGraph. Он заполняется идентификатором соответствующего объекта, если граф перехода был задан, иначе — остается не заполненным.
Пример вычитки label на SDK:
ProductPartyCollectionWith productPartyCollectionWith = ProductPartyGraph.createCollection()
.withCode()
// запрос истории статусов
.withStatusHistory(sh -> sh
.withStatus(st -> st
.withCode())
// Запрос transition вместе с атрибутом label
.withTransition(graph -> graph
.withLabel()
.withStatusFrom(st -> st
.withCode()))
);
Удаление перехода между статусами модели не приводит к удалению из БД и, как следствие, проблемы вычитки удаленных переходов из БД нет.
Особенности ведения статусной модели#
После описания потребителем статусов и переходов между статусами на модели и обработки модели генераторами, информация о статусах и переходах заносится в pdm.xml. Затем формируется Liquibase-скрипт по сохранению соответствующих данных в БД для последующей обработки и раскладки по соответствующим таблицам и базам данных.
Данные о статусах из pdm.xml используются при операциях модификации данных. При чтении используются данные о статусах, сохраненные в БД.
Изменение атрибута label приводит к обновлению соответствующих записей в pdm.xml и БД и, как следствие, к изменению значения label для уже сохраненных в историю данных, для которых проставлен идентификатор ребра перехода.
Удаление ребра графа приводит к пометке соответствующих данных в pdm.xml как deprecated, в БД данные не изменяются.
Чтение данных порциями#
Если требуется разделить читаемые данные на независимые порции (batch), можно воспользоваться методом получения хеш от значения поля. Затем к данному значению применяется операция деления с остатком на необходимое количество порций (mod).
Примечание
На разных базах данных хеш от одного и того же значения может быть разным. Однако при использовании хеш и остатка от деления на одной БД одна порция данных никогда не пересечется с другой. Метод
abs()вызывается потому, что на PostgreSQL функцияhash()может возвращать отрицательные значения.
Пример использования:
// Получение всех книг, удовлетворяющих условию "ISBN начинается с 978". Вычитываем двумя порциями, поэтому — mod(2), а не какое-то другое число.
// Первая часть:
BookCollectionWith<BookGrasp> bookCollectionWith1 = GraphCreator.selectBook()
.withAuthor()
.withName()
.setWhere(where -> where.id().hash().mod(2).abs().eq(0).and(where.isbnLike("978%")))
.setSortingAdvanced(asb -> asb.desc(grasp -> grasp.author()).desc(grasp -> grasp.name()));
// Вторая часть:
BookCollectionWith<BookGrasp> bookCollectionWith2 = GraphCreator.selectBook()
.withAuthor()
.withName()
.setWhere(where -> where.id().hash().mod(2).abs().eq(1).and(where.isbnLike("978%")))
.setSortingAdvanced(asb -> asb.desc(grasp -> grasp.author()).desc(grasp -> grasp.name()));
Мультипоиски (межшардовые поиски)#
Мультипоиск позволяет вычитывать данные, находящиеся в разных шардах. Необходимые условия:
В каждом шарде экземпляр dataspace-core развернут с одной и той же версией модели.
В шарде, на котором инициализируется запрос межшардового поиска, развернут модуль dataspace-core в режиме функционирования мультипоиска (dataspace.multisearch.enable=true).
Обобщенный алгоритм работы#
Обобщенный алгоритм работы мультипоиска состоит из следующих шагов:
Dataspace-core в режиме мультипоиска, приняв запрос потребителя, выделяет из него перечень полей сортировки, направления сортировки и информацию о пагинации.
Формируется запрос, в котором устанавливается необходимая сортировка и пагинация на основе данных исходного запроса, но выбираются только идентификаторы сущностей и значения полей сортировки.
Полученный запрос рассылается во все известные шарды.
Ответы от всех шардов объединяются и пересортируются в соответствии с критериями сортировки исходного запроса.
Выбираются идентификаторы объектов в соответствии с критериями пагинации. Определяется принадлежность выбранных объектов шардам (без обращения к CCI, на основе получения ответа из конкретного шарда).
Формируются запросы в каждый шард на получение информации по соответствующим им идентификаторам объектов. Запросы осуществляются сразу по множеству идентификаторов.
Полученные ответы объединяются в итоговый результат.
Возвращается запрошенная информация, контекст запроса для оптимизации последующих походов на следующую/предыдущую страницу и статистика ошибок вызовов шардов.
Внимание!
Мультипоиск не обеспечивает согласованность чтения ни между разными вызовами в рамках пагинации, ни в рамках одного вызова между разными шардами. Каждый запрос мультипоиска (в том числе получение очередной страницы) формирует новые запросы в шарды. В связи с этим, согласованное чтение не обеспечивается, возвращаются данные, актуальные на момент каждого обращения в шард.
Использование мультипоиска#
Для вызова запроса мультипоиска следует использовать класс DataspaceCoreMultisearchClient. Он так же, как и DataspaceCoreSearchClient, работает со спецификациями сущностей модели.
Отличия от обычного поискового клиента заключаются в том, что для работы с мультипоисками необходимо передавать контекст вызова мультипоиска для оптимизации запроса второй и последующих страниц при постраничной выборке.
Контекст мультипоиска — это объект, который можно получить через метод .getMultisearchContext() на результате поиска. Эту переменную необходимо передать в запрос следующей страницы для оптимизации.
Контекст предназначен для оптимизации перехода вперед и назад на одну страницу при пагинации, а также при повторном запросе текущей страницы при неизменности размера страницы, сортировки и условия фильтрации. Запрос выполнится без оптимизации даже в случае передачи контекста, если происходит одно из следующих событий:
меняется размер страницы;
меняется условие сортировки;
меняется условие фильтрации;
переход идет более чем на одну страницу вперед или назад. В данном случае оптимизация, которую предоставляет контекст — это запоминание позиции крайнего выбранного элемента из каждого шарда в рамках запроса.
Контекст содержит в себе следующую информацию:
на какой позиции остановилась выборка в каждом из известных шардов;
значение offset и limit, соответствующие предыдущему вызову API мультипоиска;
контрольная сумма, взятая от полей сортировки и условий фильтрации (sort и cond) запроса;
размер последней выбранной страницы — необходим для корректного перехода на страницу назад.
Пример структуры контекста в формате Json:
{
"shards": {
"shard1": 1,
"shard2": 1
},
"offset": 0,
"limit": 2,
"checksum": 23083794,
"lastPageSize": 2
}
Пример работы с клиентом мультипоиска:
DataspaceCoreMultisearchClient dataspaceCoreMultisearchClient =
new DataspaceCoreMultisearchClient(url);
BookCollectionWith<BookGrasp> bookGraspBookCollectionWith = GraphCreator.selectBook()
.withAuthor()
.withName()
.setWhere(where -> where.isbnIsNotNull())
.setSortingAdvanced(asb -> asb.desc(grasp -> grasp.author()).desc(grasp -> grasp.name()))
.setLimit(10)
.setOffset(0);
GraphCollection<BookGet> bookGetsFrom1To10 = dataspaceCoreMultisearchClient.searchBook(bookGraspBookCollectionWith, null);
MultisearchContext multisearchContext1 = bookGetsFrom1To10.getMultisearchContext();
// ...
// Работа с полученными книгами с 1 по 10 элементы
// ...
bookGraspBookCollectionWith.setOffset(10);
GraphCollection<BookGet> bookGetsFrom11To20 = dataspaceCoreMultisearchClient.searchBook(bookGraspBookCollectionWith, multisearchContext1);
MultisearchContext multisearchContext2 = bookGetsFrom11To20.getMultisearchContext();
// ...
// Работа с полученными книгами с 11 по 20
// ...
bookGraspBookCollectionWith.setOffset(20);
GraphCollection<BookGet> bookGetsFrom21To30 = dataspaceCoreMultisearchClient.searchBook(bookGraspBookCollectionWith, multisearchContext2);
MultisearchContext multisearchContext3 = bookGetsFrom21To30.getMultisearchContext();
// ... и так далее
Примечание
На данный момент мультипоиском поддерживаются только обычный поиск сущностей и запрос количества сущностей, удовлетворяющих поисковому условию. Поиск с использованием Distinct и GroupBy не поддерживается.
Также для запросов мультипоиска можно определить стратегию работы с недоступными шардами и теми, у которых модель не совместима с моделью на шард-обработчике мультипоиска.
Пример задания стратегии:
DataspaceCoreMultisearchClient dataspaceCoreMultisearchClient =
new DataspaceCoreMultisearchClient(url);
BookCollectionWith<BookGrasp> bookGraspBookCollectionWith = GraphCreator.selectBook()
.withAuthor()
.withName()
.setWhere(where -> where.isbnIsNotNull())
.setSortingAdvanced(asb -> asb.desc(grasp -> grasp.author()).desc(grasp -> grasp.name()))
.setLimit(10)
.setOffset(0);
GraphCollection<BookGet> bookGetsFrom1To10 = dataspaceCoreMultisearchClient.searchBook(bookGraspBookCollectionWith, null, strategy -> strategy.setUnavailable(UnavailableStrategy.RETRY).setRetryCount(5).setRetryIntervalMs(3000).setIncompatible(IncompatibleStrategy.IGNORE));
В блоке кода выше:
setUnavailable устанавливает стратегию при недоступности шарда. Возможные значения:
FAIL — при недоступности шарда запрос мультипоиска завершается ошибкой. Это значение используется по умолчанию;
RETRY — при недоступности шарда происходит ожидание в течение заданного промежутка времени, а затем повторный вызов;
IGNORE — при недоступности шарда запрос мультипоиска продолжает выполняться так, как будто шард вернул 0 элементов.
setRetryCount устанавливает максимальное количество повторных вызовов для стратегии RETRY. Значение по умолчанию — 5, переопределять можно только в меньшую сторону.
setRetryIntervalMs устанавливает время ожидания в миллисекундах между повторными вызовами для стратегии RETRY. Значение по умолчанию — 3000.
setIncompatible устанавливает стратегию при получении ошибки от вызываемого шарда. Возможные значения:
FAIL — при ошибке от шарда запрос мультипоиска завершается ошибкой. Это значение используется по умолчанию;
IGNORE — при ошибке от шарда запрос мультипоиска продолжает выполняться так, как будто шард вернул 0 элементов.
Внимание!
Если установлена настройка IGNORE и все шарды недоступны или вернули ошибки, то запрос мультипоиска завершится с ошибкой.
JSON-RPC-вызов мультипоиска с описанными настройками настроек выглядит так:
{
"type" : "Book",
"props" : [ "name", "author" ],
"limit" : 10,
"sort" : [ {
"crit" : "root.author"
} ],
"cond" : "root.isbn!=null",
"errorStrategy" : {
"unavailable" : "retry",
"incompatible" : "ignore",
"retryCount" : 5,
"retryIntervalMs" : 3000
}
}
Через GraphQL эти настройки передаются как один из параметров вызова:
query {
multisearchBook(sort:[{ crit: "root.author" }], cond:"root.isbn!=null", limit:10, errorStrategy:{unavailable:RETRY, incompatible: IGNORE, retryCount:5, retryIntervalMs:5000}) {
name,
author
}
}
Для того чтобы понять, сколько шардов было проигнорировано, каждый запрос мультипоиска в ответе содержит блок со статистикой по пропущенным шардам:
GraphCollection<BookGet> bookGetsFrom1To10 = multisearchClient.searchBook(...);
ShardErrorStat shardErrorStat = resultPage1.getShardErrorStat();
shardErrorStat.getPercentSkipped();
shardErrorStat.getPercentUnavailable();
shardErrorStat.getPercentIncompatible();
В блоке кода выше:
getPercentSkipped возвращает процент пропущенных шардов;
getPercentUnavailable возвращает процент шардов, пропущенных из-за недоступности;
getPercentIncompatible возвращает процент шардов, пропущенных из-за ошибок модели.
Все проценты округлены вверх до целого числа.
В ответе JSON-RPC эта информация находится на том же уровне, что и контекст.
Маршрутизация мультипоиска#
Модуль мультипоиска может отправлять запросы на шарды двумя способами:
Вызов идет напрямую, HTTP-запрос отправляется из модуля, обрабатывающего запрос, на все модули, перечисленные в настройках. Этот режим включается выставлением настройки
dataspace.multisearch.callв значениеdirect. А в настройкуdataspace.multisearch.endpointsнеобходимо передать список шардов, который является JSON-файлом формата[{"name":"shard1","url":"http://127.0.0.1:8095/"},{"name":"shard2","url":"http://127.0.0.1:8096/"}]Вызов идет с помощью некоторых внешних по отношению к DataSpace сервисов прикладного шардирования. Этот режим включается выставлением настройки
dataspace.multisearch.callв значениеasr. Также требуется задать дополнительные настройки:dataspace.multisearch.appShardEntryService.Url— URL сервиса, предоставляющего информацию о составе шардов (далее «сервис управления шардированием»).dataspace.multisearch.appShardSearchContextPath— API, по которому будет происходить подписка на получение списка шардов (shards).dataspace.multisearch.appShardEntryService.retryattempts— количество попыток подключения к «сервису управления шардированием», после которого запрос завершится с ошибкой.dataspace.multisearch.appShardEntryService.deadlineMs— время ожидания ответа от «сервиса управления шардированием», после которого будет считаться, что не удалось получить список шардов.dataspace.multisearch.appShardRouterUrl— URL сервиса маршрутизации между прикладными шардами, на который будет отправляться запрос за данными конкретного шарда.
Пользовательские SQL-запросы#
В результате объявления на модели пользовательского SQL-запроса (см. документ «Руководство по ведению модели данных») генератором модели формируются классы Graph, Grasp, Get, With, CollectionWith, позволяющие производить поиск так, как будто пользовательский SQL-запрос является классом.
Отличительной особенностью является необходимость задания параметров в случае их описания на пользовательском SQL-запросе.
Значения параметров устанавливаются при помощи методов setParam<ParamName>(), например, setParamTemplate() для параметра ${template}.
Пример применения пользовательского SQL-запроса:
Query1CollectionWith<Query1Grasp> req = Query1Graph.createCollection()
.withCode()
.withName()
.setParamTemplate(Arrays.asList("code1_1", "code2_1"))
.setSortingAdvanced(sort -> sort.asc(Query1Grasp::code));
GraphCollection<Query1Get> result = dataspaceCoreSearchClient.searchQuery1(req);
String code = result.get(0).getCode();
Примечание
Пользовательские SQL-запросы не поддерживают:
расчетные поля ($withCalculated()),
запрос уникальных значений (distinct()),
уточнение типа (extend()).
Часть вышеуказанных ограничений может быть снята (реализована) в будущем.
Примечание
В пользовательских SQL-запросах нет возможности использовать поля следующих типов:
UnicodeString(unicodestring),Text(text),Binary(binary,byte[]). Вместо них необходимо использовать типStringс требуемой длиной.Для типа
Stringнет ограничения по длине передаваемого значения.
Имеется возможность передавать null-значение в параметры, ниже приведены примеры модели и SDK.
Модель:
...
<query name="Query1" label="query label" description="query description">
<params>
<param name="codes" type="String" length="10" mask="[A-Z0-9\-]{1,10}" collection="true" label="param label" description="param description"/>
<param name="limit" type="Long"/>
<param name="localDate" type="LocalDateTime"/>
</params>
<id name="id" label="id label" description="id description"/>
<property name="code" type="String" label="property label" description="property description"/>
<property name="name" type="String"/>
<implementations>
<sql dbms="Postgresql,h2">select t1.object_id id, t1.code code, t1.name name from T_PRODUCTPARTY t1 where ${codes} is null
and ${limit} is null
and ${localDate} is null
</sql>
</implementations>
</query>
...
SDK:
...
QueryAllTypesCollectionWith<QueryAllTypesGrasp> req = QueryAllTypesGraph.createCollection()
.withCode()
.withName()
.setParamLimit(null)
.setParamLocalDate(null)
.setParamCodes(null);
GraphCollection<QueryAllTypesGet> result = dataspaceCoreSearchClient().searchQueryAllTypes(req);
...
Примеры моделей с разными SQL-запросами#
Рассмотрим пример запроса, в котором содержатся условие «<», «<=» и подобные им. Приведенные конструкции приводят к нарушению xml-разметки.
Для описания таких SQL-запросов на модели необходимо их обернуть в конструкцию <![CDATA[ ... ]]>.
Пример:
...
<implementations>
<sql dbms="Postgresql">
<![CDATA[
select t1.object_id id, t1.code code, t1.name name from T_PRODUCTPARTY t1
where number <= ${maxNumber}
]]>
</sql>
</implementations>
...
Пример запроса c использованием оператора interval на PostgreSQL:
...
<implementations>
<sql dbms="Postgresql">
select count(*) kolvo
from t_product t
where t.sys_lastChangeDate >= CURRENT_TIMESTAMP - interval '1' second * ${intParam}::int
</sql>
</implementations>
...
В конструкции выше указывается базовый интервал, который затем умножается на необходимое количество.
При этом параметр должен быть явно приведен к нужному типу (в данном случае целочисленному), иначе возникнет исключение на этапе выполнения запроса: (Неизвестный тип данных: "?").
Внимание!
При написании SQL-запроса, требующего вычислений, следует делать явное приведение типа вычисляемого выражения.
Пример использования пользовательского SQL-запроса, требующего вычислений#
Рекомендуется при написании SQL-запроса, в котором есть вычисления, явно приводить тип для избежания ошибок при использовании различных СУБД. Например:
<class name="Test" label="Тестовый класс">
<property name="fieldTime" type="OffsetDateTime"/>
</class>
<query name="QueryTest"
description="Тестовый пользовательский SQL-запрос">
<params>
<param name="time1" type="Long"/>
<param name="time2" type="Long"/>
</params>
<implementations>
<sql dbms="h2">SELECT count(test.*) count FROM t_test test
WHERE test.fieldTime BETWEEN (CURRENT_DATE - ${time1}::INTEGER) and (CURRENT_DATE - ${time2}::INTEGER)</sql>
</implementations>
<id name="id"/>
<property name="count" type="Integer"/>
</query>
В выражении (CURRENT_DATE - ${time1}::INTEGER) производится явное приведение типа.
Пример использования пользовательского SQL-запроса для решения задачи полнотекстового поиска на СУБД PostgreSQL#
Рассмотрим задачу реализации полнотекстового поиска (пример решения данной задачи на СУБД PostgreSQL).
Полнотекстовый поиск в PostgreSQL будет выглядеть следующим образом:
/* создаем тестовую таблицу и наполняем ее значениями*/
create table t_book(
name varchar(30),
author varchar(30),
isbn varchar(100) not null,
primary key(isbn)
);
insert into t_book (name, author, isbn) values ('Проблемы 21 века','Курбан Файзуллов', '0-0000-0000-1');
insert into t_book (name, author, isbn) values ('Психология и проблемы человека ','Ананьев Борис Герасимович', '0-0000-0000-2');
insert into t_book (name, author, isbn) values ('Капитанская дочка','Александр Сергеевич Пушкин', '0-0000-0000-3');
insert into t_book (name, author, isbn) values ('Капитан дальнего плавания','Александр Крон', '0-0000-0000-4');
/* выведем все таблицу */
select * from t_book;
Результатом будет следующая таблица:
name |
author |
isbn |
|---|---|---|
Проблемы 21 века |
Курбан Файзуллов |
0-0000-0000-1 |
Психология и проблемы человека |
Ананьев Борис Герасимович |
0-0000-0000-2 |
Капитанская дочка |
Александр Сергеевич Пушкин |
0-0000-0000-3 |
Капитан дальнего плавания |
Александр Крон |
0-0000-0000-4 |
Произведем полнотекстовый поиск по столбцу name:
/* найдем в данной колонке строки, содержащие слово `проблема` */
select * from t_book
where to_tsvector(cast('russian' as regconfig), name) @@ to_tsquery(cast('russian' as regconfig), 'проблема');
Результатом поиска будут следующие строки:
name |
author |
isbn |
|---|---|---|
Проблемы 21 века |
Курбан Файзуллов |
0-0000-0000-1 |
Психология и проблемы человека |
Ананьев Борис Герасимович |
0-0000-0000-2 |
Рассмотрим пример построения модели для реализации полнотекстового поиска. Для начала в model.xml создаются классы с набором необходимых свойств — будущая таблица, на основе которой будет реализован поиск:
<class name="Book" label="Книга">
<property name="bookStore" type="BookStore" label="Книжный магазин" parent="true"/>
<property name="name" type="String" label="Название"/>
<property name="author" type="String" label="Автор"/>
<property name="isbn" type="String" label="Код ISBN"/>
</class>
Далее в model.xml создается сам SQL-запрос. Пользовательский запрос будет выглядеть следующим образом:
<query name="FullTextSearch">
<params>
<param name="language" type="String" default-value="russian"/>
<param name="searchWord" type="String"/>
</params>
<implementations>
<sql>select AUTHOR as author, NAME as name, ISBN as isbn from ${dspc.schemaPrefix}T_BOOK where to_tsvector(cast(${language} as regconfig), NAME) @@ to_tsquery(cast(${language} as regconfig), ${searchWord})</sql>
</implementations>
<property name="author" type="String"/>
<property name="name" type="String"/>
<property name="isbn" type="String"/>
</query>
Внимание!
Необходимо явно прописывать схему перед именем таблицы.
В параметрах запроса задается язык и слово, по которому будет происходить поиск. Для языка выставлено значение по умолчанию.
Для генерации данных воспользуемся SDK:
public static void createBookStore() throws Throwable {
Packet creatingPacket = new Packet();
// Создание книжного магазина
BookStoreRef chitayGorodBookStoreRef = creatingPacket.bookStore.create(bookstore -> {
bookstore.setAddress("Большая Садовая ул., 110 стр. 131, Ростов-на-Дону");
bookstore.setName("Читай-город");
});
// Создание книги 1
BookRef bookRef1 = creatingPacket.book.create(book -> {
book.setAuthor("Курбан Файзуллов");
book.setName("Проблемы 21 века");
book.setIsbn("0-0000-0000-1");
book.setBookStore(chitayGorodBookStoreRef);
});
// Создание книги 2
BookRef bookRef2 = creatingPacket.book.create(book -> {
book.setAuthor("Ананьев Борис Герасимович");
book.setName("Психология и проблемы человекознания ");
book.setIsbn("0-0000-0000-2");
book.setBookStore(chitayGorodBookStoreRef);
});
// Создание книги 3
BookRef bookRef3 = creatingPacket.book.create(book -> {
book.setAuthor("Александр Сергеевич Пушкин");
book.setName("Капитанская дочка");
book.setIsbn("0-0000-0000-3");
book.setBookStore(chitayGorodBookStoreRef);
});
// Создание книги 4
BookRef bookRef4 = creatingPacket.book.create(book -> {
book.setAuthor("Александр Крон");
book.setName("Капитан дальнего плавания");
book.setIsbn("0-0000-0000-4");
book.setBookStore(chitayGorodBookStoreRef);
});
// Запуск пакета на исполнение
dataspaceCorePacketClient.execute(creatingPacket);
}
Наполненная таблица будет выглядеть следующим образом:
bookstore_id |
name |
author |
isbn |
object_id |
aggregateroot_id |
type |
chgcnt |
sys_isdeleted |
sys_lastchangedate |
offflag |
sys_ownerid |
sys_partitionid |
sys_recmodelversion |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
7128360944985767937 |
Проблемы 21 века |
Курбан Файзуллов |
0-0000-0000-1 |
7128360944985767938 |
7128360944985767937 |
Book |
false |
2022-08-05 15:01:07.685 |
0 |
||||
7128360944985767937 |
Психология и проблемы человекознания |
Ананьев Борис Герасимович |
0-0000-0000-2 |
7128360944985767939 |
7128360944985767937 |
Book |
false |
2022-08-05 15:01:07.686 |
0 |
||||
7128360944985767937 |
Капитанская дочка |
Александр Сергеевич Пушкин |
0-0000-0000-3 |
7128360944985767940 |
7128360944985767937 |
Book |
false |
2022-08-05 15:01:07.687 |
0 |
||||
7128360944985767937 |
Капитан дальнего плавания |
Александр Крон |
0-0000-0000-4 |
7128360944985767941 |
7128360944985767937 |
Book |
false |
2022-08-05 15:01:07.687 |
0 |
Сам поиск:
public void fullTextSearchBook() throws Throwable{
String searchWord="проблема"; // Ключевое слово для поиска
//Делаем настойку поискового запроса
FullTextSearchCollectionWith<FullTextSearchGrasp> cw=FullTextSearchGraph.createCollection()
.withAuthor()
.withName()
.withIsbn()
.setParamSearchWord(searchWord);
//Производим поиск по заданным параметрам
GraphCollection<FullTextSearchGet> result=dataspaceCoreSearchClient.searchFullTextSearch(cw);
//Вывод полученных значений в консоль
System.out.println("//////////////////////////////");
for(FullTextSearchGet p:result.getCollection()){
System.out.println(p.getAuthor());
System.out.println(p.getName());
System.out.println(p.getIsbn());
}
System.out.println("//////////////////////////////");
}
В результате поиска в консоль выведется следующая информация:
//////////////////////////////
Курбан Файзуллов
Проблемы 21 века
0-0000-0000-1
Ананьев Борис Герасимович
Психология и проблемы человекознания
0-0000-0000-2
//////////////////////////////
Историцирование (получение данных)#
Внимание!
Прежде чем применить функциональность историцирования, ознакомьтесь с подразделом «Коллизии историцирования» текущего документа.
Описание включения функции историцирования приведено в документе «Руководстве по ведению модели данных». Для сохранения информации о пользователе, внесшем изменения, можно воспользоваться функциональностью, описанной в разделе «Сохранение в историю пользователя, изменившего данные» документа «Руководство по системному администрированию».
Работать с историческими данными можно посредством традиционных поисковых запросов, а также при помощи API,
предоставляемого специальным поисковым клиентом DataspaceCoreHistoryClient.
Прежде чем перейти к работе с историческими данными, необходимо понимать, как они ведутся в БД. Это особенно актуально, если запрос исторических данных будет производиться через обычные поисковые запросы DataSpace, а не через API историцирования.
Имеются следующие особенности историцирования:
Время изменения для истории берется как текущее время БД (за исключением сохранения «старых» данных). Под сохранением старых данных понимается сохранение предыдущих значений в момент изменения объекта для новых историцируемых полей.
Ввиду того, что в DataSpace применяется
dynamicUpdate(т.е. в БД сохраняются (изменяются) только измененные потребителем атрибуты сущности, а не вся сущность целиком) и с целью уменьшения занимаемого историческими данными дискового пространства в историю записываются только измененные атрибуты сущности. Другими словами каждая запись в таблице истории содержит значения только для изменившихся атрибутов сущности, для не изменившихся атрибутов сущности значение — «null».Для того чтобы отделить установленное значение атрибута «null» от значения «null», как значения не изменившегося атрибута, для каждого исторического поля добавляется поле-флаг, содержащее:
«1» (true) — соответствующий атрибут сущности был изменен;
значение «null» — соответствующий атрибут сущности не изменялся. Имя поля соответствует следующему формату: sys<имя историцируемого атрибута$gt;Updated.
Атрибут, который на начало и конец транзакции имеет одно и то же значение, считается не изменившимся, даже если в процессе транзакции (пакета) он изменял свое значение несколько раз.
В рамках одной транзакции (пакета) для каждой историцируемой сущности (сущности с историцируемыми полями) сохраняется ровно одна запись в таблицу истории в БД.
Каждая запись истории обладает следующими дополнительными атрибутами:
sysHistoryOwner— ссылка на измененный объект (владелец истории);sysHistoryTime— время изменения данных;sysState— признак изменения (0 — создание, 1 — обновление, 2 — удаление, 3 — сохранение устаревших значений).sysHistNumber— номер истории (каждая последующая запись имеет номер выше, чем предыдущая в рамках агрегата, но не обязательно последовательно).
Если историцирование включается для уже созданных сущностей, или происходит расширение перечня историцируемых полей сущности, то при очередном обновлении сущности «старые» значения для новых историцируемых полей записываются в историю отдельной записью на время предыдущего изменения сущности со значением sysState=3.
Пример записи в таблице истории:
object_id |
sysHistoryTime |
sysHistoryOwner |
sysHistNumber |
sysState |
code |
sysCodeUpdated |
name |
sysNameUpdated |
|---|---|---|---|---|---|---|---|---|
865423215546687 |
2021-10-05T18:45:38.785412Z |
884321697854135 |
25 |
1 |
„someCode“ |
true |
null |
null |
Ограничения:
Отсутствует возможность получения истории связанных сущностей (вложенных сущностей). Имеется возможность получить исторические данные для каждой из связанных (вложенных) сущностей отдельно.
Отсутствует возможность историцирования binary-, text-полей, embedded-объектов и их полей (за исключением внешних ссылок), обратных ссылок и любых коллекционных полей.
Получение данных историцирования через клиента (DataspaceCoreHistoryClient)#
Термины:
Состояние сущности — наличие значений для всех запрошенных атрибутов сущности.
Изменение сущности — отдельная строка в таблице изменений по соответствующей сущности. Значения имеются только у изменившихся атрибутов, у остальных — «null».
Поисковый клиент историцирования DataspaceCoreHistoryClient предоставляет API для работы с данными только одной сущности. Для работы с историчными данными сразу нескольких сущностей необходимо использовать обычный поисковый клиент DatspaceCoreSearchClient. При этом необходимо будет учитывать специфику хранения данных, описанную выше.
Поисковый клиент историцирования DataspaceCoreHistoryClient предоставляет следующие API:
Получение состояния сущности на заданный момент времени (заданную версию).
Получение списка состояний заданной сущности за период времени (диапазон версий, по одному состоянию на каждое изменение).
Получение списка изменений заданной сущности за период времени (getHistory). Данный API может быть заменен традиционным поисковым запросом.
Определение запрашиваемых данных#
Для описания запрашиваемых исторических данных необходимо воспользоваться классом с именем \[ModelClassName\]HistoryGraph, на котором требуется вызвать метод .createCollection(). После этого необходимо вызвать with-методы, соответствующие запрашиваемым полям. Например, для запроса поля Code необходимо вызвать метод .withCode().
Для запроса признака обновления поля необходимо вызвать метод .withSys\[PropName\]Updated. Например, для запроса признака обновления поля Code необходимо вызвать метод .withSysCodeUpdated().
Внимание!
Признак Updated для записей старых значений (sysState = 3) не означает, что поле было обновлено, а служит признаком установленного значения, чтобы отделить значение поля «null» от null, как отсутствия данных. Для такой записи невозможно определить, изменялось ли поле, так как на соответствующий ей момент данные поля еще не историцировались и изменения по ним не отслеживались.
Для запроса признака успешности вычисления поля необходимо вызвать метод .withSys\[PropName\]Calculated. Например, для запроса признака успешности определения значения поля Code необходимо вызвать метод .withSysCodeCalculated().
Примечание
Признак успешности вычисления позволяет отличить действующие значения «null» атрибутов сущностей от значения null, как неудавшейся попытки определить значения поля (в случае отсутствия данных в истории).
Также имеется возможность запроса следующих системных полей:
.withSysHistoryTime()— запрос поля времени изменения;.withSysState()— запрос поля признака изменения (создан, обновлен, удален, запись «старых» значений);.withSysHistoryOwner()— запрос ссылки на владельца истории (изменяемый объект);.withSysHistNumber()— запрос номера истории.
Пример объявления запрашиваемых полей:
DepositCBExmplHistoryCollectionWith<? extends DepositCBExmplHistoryGrasp> collectionWith =
DepositCBExmplHistoryGraph.createCollection()
.withCode()
.withSysCodeUpdated()
.withSysCodeCalculated()
.withDeclaration()
.withSysHistoryTime();
В зависимости от вызываемой API историцирования перечень допустимых для запроса полей изменяется.
Так для API определения состояния на момент времени не допускается запрос:
признаков Updated;
sysHistoryTime;
sysState;
sysHistNumber.
Для API списка состояний сущности допускается запрос всех полей и признаков.
Для API списка изменений сущности не допускается запрос признаков Calculated.
Внимание!
Несмотря на то, что объект
HistoryGraphпозволяет вызвать дополнительные методы (например, для установки фильтрации.setWhere()или сортировки.setSortingAdvanced()), вызов таких методов не допускается в запросах, которые будут использованы при вызове исторических API. Традиционная сортировка в исторических API не допускается и реализована через параметры вызова API.
Получение состояния сущности на заданный момент времени (заданную версию)#
Данный API возвращает информацию о значениях всех указанных в запросе историцируемых атрибутах указанного объекта на заданный момент времени или заданную версию (если информации в истории достаточно для их расчета).
Сигнатура API в SDK:
Optional<<EntityName>HistoryGet> dataspaceCoreHistoryClient.<EntityName>HistoryState(<EntityName>HistoryCollectionWith propReq, String entityId, OffsetDateTime time) throws SdkJsonRpcClientException, где:propReq— запрос историцируемых свойств. Такой же объект, как и при обычных поисках, при этом он должен содержать только описание запрашиваемых полей. Расчетные поля, условия фильтрации и сортировки не допускаются.entityId— идентификатор сущности, для которой требуется определить состояние.time— время, на которое требуется определить состояние (обязательный параметр).
Optional<<EntityName>HistoryGet> dataspaceCoreHistoryClient.<EntityName>HistoryState(<EntityName>HistoryCollectionWith propReq, String entityId, Long histNumber) throws SdkJsonRpcClientException. В отличие от предыдущего метода последним параметром идетLong(вместоOffsetDateTime), задающий номер версии изменения С версии Platform V DataSpace 1.14.0 совпадает с новым номером версии агрегата, ранее совпадение было примерным — рассчитывалось до блокирования версии агрегата). Данный метод может быть использован для выборки состояния поHistoryEvent(событие создания записи в истории сущности).
Если методу не удалось рассчитать ни одного запрошенного поля, то возвращается «null».
Если методу удается рассчитать хотя бы одно поле, то возвращается результат. При этом поля, для которых не удалось рассчитать значение, будут содержать «null». Отличить действующее значение «null» поля от ситуации, когда значение не удалось рассчитать, можно, если был дополнительно запрошен признак sys\[PropName\]Calculated:
Если признак имеет значение «true», то «null» — действующее значение поля.
Если признак имеет значение «false», то значение поля рассчитать не удалось (в истории не хватает данных).
Пример вызова API:
DepositCBExmplHistoryCollectionWith<? extends DepositCBExmplHistoryGrasp> collectionWith =
DepositCBExmplHistoryGraph.createCollection()
.withCode()
.withCodeCalculated()
.withDeclaration()
.withDeclarationCalculated();
Optional<DepositCBExmplHistoryGet> stateResult
= dataspaceCoreHistoryClient().depositCBExmplHistoryState(collectionWith, objectId, time);
Получение списка состояний сущности за период времени (диапазон версий)#
API возвращает состояние сущности на каждое изменение внутри заданного интервала времени (диапазон версий), включая информацию о сохраненных старых значениях.
Сигнатура API в SDK:
GraphCollection<<EntityName>HistoryGet> historyTestingHistoryStates(<EntityName>HistoryCollectionWith propReq, HistorySearchSpecification searchSpec) throws SdkJsonRpcClientException
В вышеуказанном выражении:
propReq— запрос историцируемых свойств. Такой же объект, как и при обычных поисках, при этом объект должен содержать только описание запрашиваемых полей. Расчетные поля, условия фильтрации и сортировки не допускаются.searchSpec— дополнительные параметры запроса передаются при помощиHistorySearchSpecificationImpl— для ограничения выборки по времени илиHistorySearchSpecificationByHistNumberImpl— для ограничения выборки поhistNumber.
Обязательными для выполнения функциями являются следующие параметры: идентификатор объекта, время начала и конца периода поиска данных.
HistorySearchSpecification имеет две имплементации: HistorySearchSpecificationImpl — для ограничения по времени и HistorySearchSpecificationByHistNumberImpl — для ограничения по диапазону версий.
HistorySearchSpecificationImpl имеет следующие методы:
create()— создает пустой объект;create(String entityId)— создает объект. устанавливая фильтр по идентификатору;create(String entityId, OffsetDateTime timeFrom, OffsetDateTime timeTo)— создает объект с установкой фильтра по идентификатору и интервалу времени;setEntityId(String entityId)— устанавливает идентификатор искомой сущности;setLimit(Integer limit)— устанавливает ограничения на количество выбираемых записей (пагинация);setOffset(Integer offset)— устанавливает смещения выбираемых записей (пагинация);setTimeFrom(OffsetDateTime timeFrom)— устанавливает время, с которого будут искаться изменения данных;setTimeTo(OffsetDateTime timeTo)— устанавливает время, по которое будут искаться изменения данных;setNeedCount(Boolean needCount)— устанавливает признак запроса общего количества записей, удовлетворяющих критериям поиска;setSortDirection(SortingType sortDirection)— устанавливает направление сортировки данных в прямом или обратном хронологическом порядке (по умолчанию — прямой порядок).
HistorySearchSpecificationByHistNumberImpl вместо методов setTimeFrom и setTimeTo имеет методы setHistNumberFrom
и setHistNumberTo для установки версий начала и окончания выборки соответственно.
Пример вызова API:
DepositCBExmplHistoryCollectionWith<? extends DepositCBExmplHistoryGrasp> collectionWith =
DepositCBExmplHistoryGraph.createCollection()
.withCode()
.withCodeUpdated()
.withCodeCalculated()
.withDeclaration()
.withDeclarationUpdated()
.withDeclarationCalculated()
.withSysHistoryTime()
.withSysHistNumber()
.withSysState();
GraphCollection<DepositCBExmplHistoryGet> result
= dataspaceCoreHistoryClient().depositCBExmplHistoryStates(
collectionWith,
HistorySearchSpecificationImpl.create(objectId, timeFrom, timeTo)
.setSortDirection(SortingType.DESC)
.setNeedCount(true)
);
Получение списка изменений сущности за период времени#
Данная API возвращает список изменений заданной сущности за запрошенный интервал времени. Отличие от API получения списка состояний в том, что, если атрибут не изменялся в соответствующей записи транзакции, то его значение будет «null».
Работа данной API может быть заменена традиционным поисковым запросом по таблице с историческими данными.
Сигнатура API в SDK:
GraphCollection<<EntityName>HistoryGet> historyTestingHistory(<EntityName>HistoryCollectionWith cwGraph, HistorySearchSpecification searchSpec) throws SdkJsonRpcClientException {
return getHistorical(HistoryTestingHistoryGet.class, cwGraph, searchSpec);
}
Параметры запроса аналогичны параметрам запроса для API списка состояний.
Пример вызова API:
DepositCBExmplHistoryCollectionWith<? extends DepositCBExmplHistoryGrasp> collectionWith =
DepositCBExmplHistoryGraph.createCollection()
.withNumAb()
.withDeclaration()
.withSysHistoryOwner()
.withSysHistoryTime();
GraphCollection<DepositCBExmplHistoryGet> result
= dataspaceCoreHistoryClient().depositCBExmplHistory(
collectionWith,
HistorySearchSpecificationImpl.create(
objectId,
startTime,
stopTime
).setNeedCount(true)
);
Получение состояния для коллекции однотипных сущностей на заданный момент времени по идентификаторам#
Данный API позволяет получить состояние однотипных сущностей на заданное время по списку идентификаторов сущностей.
В API передается коллекция идентификаторов и время, на которое требуется определить состояние.
Что бы использовать API необходимо добавить параметр historyStateManyEntities в плагин SDK и внутри параметра указать
один или несколько классов через запятую для которых хотите сгенерировать API.
К примеру:
<plugin>
<groupId>sbp.com.sbt.dataspace</groupId>
<artifactId>model-api-generator-maven-plugin</artifactId>
<executions>
<execution>
<id>createModel</id>
<goals>
<goal>createModel</goal>
</goals>
<configuration>
...
<historyStateManyEntities>Product,Service</historyStateManyEntities>
</configuration>
</execution>
</executions>
</plugin>
Аналогичная настройка должна быть указана для <goal>createSdk</goal>.
Специфика работы нового API
Сигнатура API в SDK:
HistBaseHistoryCollectionWith<? extends HistBaseHistoryGrasp> req
= HistBaseHistoryGraph.createCollection()
.withCode()
.withSysCodeCalculated()
.withName()
.withSysNameCalculated()
.withAttrD()
.withSysAttrDCalculated()
.withAttrL()
.withSysAttrLCalculated()
.withAttrS()
.withSysAttrSCalculated()
.withAttrE()
.withSysAttrECalculated()
.withAttrLD()
.withSysAttrLDCalculated()
.withAttrLDT()
.withSysAttrLDTCalculated()
.withAttrO()
.withSysAttrOCalculated()
.withEmb(emb -> emb
.withAttrL()
.withSysAttrLCalculated()
.withAttrS()
.withSysAttrSCalculated()
)
.withRequestRef(ref -> ref
.withEntityId()
.withSysEntityIdCalculated()
);
GraphCollection<ServiceHistoryGet> resp = dataspaceCoreHistoryClient().serviceHistoryStateMany(req,
HistorySearchOwnersStateByTimeSpecificationImpl.create(
// Передаются идентификаторы сущностей
List.of("a1b2c3d4e5f678901234567890abcdef", "b8d0f1a2e9c745673d21a0c5e7f9d4b1"),
someOffsetDateTime
)
);
Сигнатура API на протоколе GraphQL:
Название запроса строится по шаблону: getStateMany + постфикс, соответствующий сущности, к которой применяется операция.
Для сущности Service название будет getStateManyService.
query {
getStateManyService(
id: ["a1b2c3d4e5f678901234567890abcdef", "b8d0f1a2e9c745673d21a0c5e7f9d4b1"],
date: "2025-05-23T17:41:32.125Z"
) {
elems {
sysHistoryOwner
code
attrD
attrL
attrS
attrE
attrLD
attrLDT
attrO
emb {
attrL
attrS
}
requestRef {
entityId
}
}
}
}
Пример запроса для получения списка идентификаторов сущностей, связанных с заданной, на указанный момент времени
В примере ниже осуществляется поиск идентификаторов сущностей Service, связанных с сущностью Product на момент времени someOffsetDateTime.
ServiceHistoryCollectionWith<? extends ServiceHistoryGrasp> req = ServiceHistoryGraph.createCollection()
.withSysHistoryOwner()
.setWhere(where ->
// Время изменения сущностей меньше или равно указанному
where.sysHistoryTime().lessOrEq(someOffsetDateTime)
// значение поле связи указывает на заданную сущность
.and(where.parent().objectId().eq(productRef.getId()))
// и не существует записи в истории после найденной, которая меняет значение поля ссылки, или
// свидетельствует об удалении сущности вплоть до указанного времени
.and(GraspHelper.not(
where.getEntitiesCollections().ServiceHistoryExists(
filter -> filter.sysHistoryOwner().objectId().eq(where.sysHistoryOwner().objectId())
.and(filter.sysHistoryTime().greater(where.sysHistoryTime()))
.and(filter.sysHistoryTime().lessOrEq(someOffsetDateTime))
.and(
filter.sysParentUpdated().eq(true)
.or(
filter.sysState().eq(EntityState.DELETED.getState())
)
)
)
))
);
GraphCollection<ServiceHistoryGet> res = dataspaceCoreSearchClient().serviceHistory.search(req);
List<String> ids = res.stream().map(it -> it.getSysHistoryOwner().getObjectId()).toList();
Поиск по историческим данным#
Если какой-либо атрибут сущности помечен историцируемым, то для всей цепочки наследования (вверх и вниз по иерархии) создаются исторические классы.
Названия исторического класса формируется путем добавления суффикса History к имени сущности.
Например, для Product исторический класс будет иметь имя ProductHistory.
При построении поискового запроса по историческим данным необходимо учитывать специфику их хранения, описанную в начале раздела.
Пример поискового запроса по историческим данным:
DepositCBExmplHistoryCollectionWith<? extends DepositCBExmplHistoryGrasp> histSearch =
DepositCBExmplHistoryGraph.createCollection()
.withNumAb()
.withSysNumAbUpdated()
.withDeclaration()
.withSysDeclarationUpdated()
.withSysHistoryTime()
.setTotalCount(true)
.setWhere(it -> it.sysDeclarationUpdatedEq(true).and(it.declarationLike(prefix + "%"))
.or(it.sysNumAbUpdatedEq(true).and(it.numAbLike(prefix + "%"))))
.setSortingAdvanced(sort -> sort.asc(it -> it.sysHistoryTime()));
GraphCollection<DepositCBExmplHistoryGet> result = dataspaceCoreSearchClient().searchDepositCBExmplHistory(histSearch);
Сохранение устаревших данных#
Если историцирование включается на уже существующих данных, или к историцированию добавляется уже существующий и заполненный в БД столбец, то в момент изменения сущности устаревшее значение (значение до изменения) записывается в историю отдельной записью с признаком sysState=3. Время изменения определяется как время последнего изменения сущности (из истории или поля lastChangeDate объекта). Номер истории определяется как последний номер истории сущности, или же -1, если сущность ранее не историцировалась. Таким образом, в истории могут существовать две записи с одинаковым
sysHistNumber: одна запись соответствует изменению сущности, вторая запись соответствует сохранению старых значений.
Примечание
Время сохранения старых значений смещено относительно времени изменения на 1 микросекунду (или 1000 наносекунд).
Историцирование внешних ссылок (reference полей классов модели)#
В модели данных DataSpace реализована возможность историцирования внешних ссылок (всех 3 видов) путем установки на поле флага historical=true.
Пример модели с историцируемой внешней ссылкой:
<class name="ProductParty" label="Продукт">
<property name="series" type="String" label="Какие-то серии" historical="true"/>
<property name="num" type="String" label="Какие-то номер" historical="true"/>
<property name="comment" type="String" label="Какие-то комментарий"/>
<!-- историцируемая ссылка на корень агрегата -->
<reference name="externalRequest" type="RequestInst" historical="true"/>
<!-- историцируемая ссылка на лист агрегата -->
<reference name="externalService" type="PerformedService" historical="true"/>
<!-- историцируемая ссылка на внешний тип данных -->
<reference name="client" type="Client" historical="true"/>
</class>
<class name="PerformedService" label="Исполняемый сервис">
<property name="request" type="RequestInst" parent="true"/>
<!-- остальное содержимое класса опущено, т.к. не имеет значения в рамках задачи -->
</class>
<class name="RequestInst" label="Запрос">
<property name="services" type="PerformedService" collection="set" mappedBy="request"/>
<!-- остальное содержимое класса опущено, т.к. не имеет значения в рамках задачи -->
</class>
Использование API на тестовой модели:
OffsetDateTime time = OffsetDateTime.now();
ProductPartyHistoryCollectionWith<? extends ProductPartyHistoryGrasp> productHistoryCollectionWith = ProductPartyHistoryGraph.createCollection()
.withExternalRequest(it -> it.withEntityId())
.withExternalService(it -> it.withEntityId().withRootEntityId())
.withClient(it -> it.withEntityId());
Optional<ProductPartyHistoryGet> resHistoryState = dataspaceCoreHistoryClient.productPartyHistoryState(productHistoryCollectionWith, "entityId", time);
String linkHistoryState = resHistoryState.get().getExternalRequest().getEntityId();
GraphCollection<ProductPartyHistoryGet> resHistoryStates = dataspaceCoreHistoryClient.productPartyHistoryStates(productHistoryCollectionWith, HistorySearchSpecificationImpl.create("entityId"));
String linkHistoryStates1 = resHistoryStates.get(0).getExternalService().getEntityId();
String linkHistoryStates2 = resHistoryStates.get(0).getExternalRequest().getEntityId();
String linkRootHistoryStates2 = resHistoryStates.get(0).getExternalService().getRootEntityId();
String linkHistoryStates3 = resHistoryStates.get(0).getClient().getEntityId();
GraphCollection<ProductPartyHistoryGet> resHistory = dataspaceCoreHistoryClient.productPartyHistory(productHistoryCollectionWith, HistorySearchSpecificationImpl.create("entityId"));
String linkHistory1 = resHistory.get(0).getExternalService().getEntityId();
String linkHistory2 = resHistory.get(0).getExternalRequest().getEntityId();
String linkRootHistory2 = resHistory.get(0).getExternalService().getRootEntityId();
String linkHistory3 = resHistory.get(0).getClient().getEntityId();
Коллизии историцирования#
При работе функции историцирования могут возникать следующие коллизии:
Коллизия обновления модулей.
Коллизия перехода в StandIn.
Коллизия миграции данных.
Коллизия времени изменения данных.
Коллизия обновления модулей#
При обновлении модулей DataSpace c изменением модели, а именно — с изменениями в историцировании, может возникнуть коллизия по данным историцирования. Так как обновление осуществляется в большинстве случаев с помощью механизма rolling-update, то возникает ситуация, когда в один и тот же момент времени работают как версии со старой моделью, так и версии с новой моделью. Если в версии с новой моделью были добавлены историцируемые атрибуты, то может возникнуть ситуация, когда обновления некоторой часто обновляемой сущности сначала обработает модуль DataSpace Core с новой моделью, где поле историцируется, а затем — модуль DataSpace Core со старой моделью, где это же поле еще не историцируется. В результате модуль со старой моделью не сохранит информацию в историю по такому полю и данные будут иметь расхождение до очередного обновления данного поля модулем с новой моделью.
При этом стоит отметить, что один запрос на изменение данных (один пакет) обрабатывается ровно одним модулем DataSpace. Поэтому данная коллизия не может возникнуть в пределах обработки одного пакета изменения данных.
Данная коллизия может быть исключена или значительным образом уменьшена вероятность ее возникновения, если проводить обновления модели DataSpace в период наименьшей активности изменения данных.
Коллизия перехода в StandIn#
Данная коллизия связана с переходом в StandIn и обратно. При записи исторической информации в качестве времени изменения берется время БД (за исключением сохранения старых данных). При этом время основной и StandIn БД могут несколько отличаться. В результате, если историцируемая сущность часто обновляется, то при переходе в StandIn или обратно может возникнуть ситуация, когда изменение, произведенное позже, будет записано в БД с временем раньше, чем предыдущее.
Уменьшить вероятность возникновения данной коллизии возможно за счет своевременной синхронизации времен основной и StandIn БД, а также недопущения большой рассинхронизации по времени между ними. Другими словами, время переключения в StandIn должно превышать интервал рассинхронизации между временами БД.
Определить такую коллизию поможет поле sysHistNumber, которое возрастает в каждой новой записи истории.
Коллизия миграции данных#
Данная коллизия связана с переносом данных из одного шарда (БД) в другую. Коллизия может быть связана как с рассинхронизацией времени между БД, так и с отличием модели данных в новом шарде. Например, в новом шарде могли еще не применить новую модель данных. Поэтому возникает ситуация, когда данные переносятся из шарда, в котором велось историцирование, в шард, где историцирование еще не ведется (или наоборот).
Данная коллизия решается только административным путем — отслеживанием того, что на всех шардах установлена одинаковая модель.
Коллизия времени изменения данных#
Несмотря на то, что время изменения данных берется из БД (за исключением времени сохранения старых значений), время самой БД может изменяться в рамках синхронизации. Например, между двумя синхронизациями времени время БД уйдет вперед на 1 минуту. После синхронизации времени одна и та же минута будет пройдена дважды. В результате последующее изменение может иметь меньшее время, чем предыдущее.
Определить такую коллизию поможет поле sysHistNumber, которое возрастает с каждой новой историей в рамках агрегата.
Вопросы и ответы по использованию поискового SDK#
Почему в окне debug ссылочные поля и поля коллекции заполнены значением «null», хотя в спецификации поиска я их заказал и в БД данные есть?
Ссылочные поля и поля коллекции (как коллекции объектов, так и коллекции примитивных элементов) инициализируются в момент обращения к ним. Т.е. чтобы в окне отладки (debug) увидеть значение ссылочных и коллекционных атрибутов, необходимо для них вызвать метод get.
При ленивой инициализации ссылочных и коллекционных полей осуществляются ли дополнительные запросы к серверу?
Не осуществляются. Клиенту приходит вся информация по запросу. Лениво осуществляется парсинг полученного json по мере необходимости.
DataSpace-клиенты в DataSpace Java SDK#
В рамках модуля DataSpace Java SDK на данный момент доступно четыре DataSpace-клиента:
DataspaceCoreSearchClient;DataspaceCorePacketClient;DataspaceCoreHistoryClient;DataspaceCoreMultisearchClient.
Их использование позволяет взаимодействовать с сервисом DataSpace Core по протоколу JSON-RPC 2.0 в удобном и типизированном формате.
Примечание
Рекомендуется использовать один экземпляр на приложение для каждого DataSpace-клиента.
Конфигурация DataSpace-клиентов#
У каждого из доступных DataSpace-клиентов присутствует конструктор со следующими параметрами:
<clientClassName>(String url)
При использовании данного конструктора для инициализации DataSpace-клиента будет задана конфигурация со значениями параметров по умолчанию, которые будут описаны ниже.
Если требуется дополнительная конфигурация DataSpace-клиента, то необходимо использовать конструктор, принимающий следующие параметры:
<clientClassName>(String url, DataspaceClientConfiguration configuration)
Конфигурировать клиенты можно, используя конкретные реализации DataspaceClientConfiguration:
DataspaceSdkApiClientConfiguration— при использовании зависимости sdk-api;DataspaceSdkApiLiteClientConfiguration— при использовании зависимости sdk-api-lite.
Целевой способ инициализации DataSpace-клиентов при использовании зависимости sdk-api:
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl(url)
//Рекомендуется конфигурировать данный параметр для того, чтобы установить значения тайм-аутов
//ReactorClientHttpConnector является целевым
//Можно использовать NettyClientHttpConnectorBuilder из состава DataSpace Java SDK(позволяет писать меньше кода)
//Можно использовать стандартный способ из официальной документации: https://docs.spring.io/spring-framework/docs/5.1.2.RELEASE/spring-framework-reference/web-reactive.html#webflux-client
.clientConnector(NettyClientHttpConnectorBuilder.create()
.readTimeoutMs(60_000)
.connectionTimeoutMs(60_000)
.build()
)
//При конфигурации WebClient по умолчанию, данный параметр равен 256KB, рекомендуется установить больше,
//чтобы избежать ошибок при чтении данных большого объема (задается в байтах)
.exchangeStrategies(ExchangeStrategies.builder().codecs(
config -> config.defaultCodecs().maxInMemorySize(1_000_000)).build()
).build();
}
@Bean
public DataspaceCorePacketClient packetClient(WebClient webClient) {
return new DataspaceCorePacketClient(
url,
DataspaceSdkApiClientConfiguration.of(builder -> builder.setWebClient(webClient))
);
}
@Bean
public DataspaceCoreSearchClient searchClient(WebClient webClient) {
return new DataspaceCoreSearchClient(
url,
DataspaceSdkApiClientConfiguration.of(builder -> builder.setWebClient(webClient))
);
}
@Bean
public DataspaceCoreMultisearchClient multisearchClient(WebClient webClient) {
return new DataspaceCoreMultisearchClient(
url,
DataspaceSdkApiClientConfiguration.of(builder -> builder.setWebClient(webClient))
);
}
@Bean
public DataspaceCoreHistoryClient historyClient(WebClient webClient) {
return new DataspaceCoreHistoryClient(
url,
DataspaceSdkApiClientConfiguration.of(builder -> builder.setWebClient(webClient))
);
}
Внимание!
При использовании данного способа конфигурации, функциональность, описанная в разделе Статистика утилизации пула соединений HTTP-клиента, не будет доступна.
Сбор метрик HTTP-клиента необходимо организовывать в соответствии с информацией из раздела Метрики HTTP-клиента.
При работе с большими строками может возникать ошибка Exceeded limit on max bytes to buffer : 262144, связанная с ограничением используемого по умолчанию декодера.
Вариант решения:
@Bean
WebClient webClient() {
return WebClient
.builder()
.exchangeStrategies(ExchangeStrategies.builder().codecs(
config -> {
config.defaultCodecs().maxInMemorySize(Integer.MAX_VALUE);
config.defaultCodecs().jackson2JsonDecoder(
new Jackson2JsonDecoder(
JsonMapperBuilder.create().build(),
MediaType.APPLICATION_JSON)
);
}
)
.build()
)
.build();
}
Далее описаны настройки, которые можно конфигурировать при инициализации DataSpace-клиентов, на примере DataspaceCorePacketClient.
Конфигурация DataspaceCorePacketClient при использовании зависимости sdk-api:
DataspaceCorePacketClient packetClient =
new DataspaceCorePacketClient("http://localhost:8080",
DataspaceSdkApiClientConfiguration.of(builder ->
builder
//Инстанс WebClient, который будет использоваться для HTTP-вызовов
//Использование данного параметра является целевым
//Необходимо объявить Spring bean WebClient, подробнее описано выше
//Значение по умолчанию: null.
//Начиная с версии dataspace 1.12 значение baseUrl экземпляра webClient переопределяется
//значением соответствующего Dataspace..Client. Для переопределения поведения используется
//метод setOverrideWebClientBaseUrl(false) билдера DataspaceSdkApiClientConfiguration.
.setWebClient(webClient)
//Настройка тайм-аута соединения для HttpClients
//Значение по умолчанию: 60_000
.setConnectionTimeoutMs(10_000)
//Настройка тайм-аута чтения для HttpClients
//Значение по умолчанию: 60_000
.setReadTimeoutMs(10_000)
//Настройка SSL для HttpClients
//Значение по умолчанию: SSLConfiguration.DEFAULT
.setSSLConfiguration(SSLConfiguration.of(trustStore))
//Настройка токена безопасности для всех вызовов, выполняемых данным DataSpace-клиентом
//Значение по умолчанию: SecurityInfo.NOT_SET
.setSecurityInfo(SecurityInfo.NOT_SET)
//Настройка взаимодействия с ApiGateway для всех вызовов, выполняемых данным DataSpace-клиентом
//Значение по умолчанию: ApiGatewayConfiguration.NOT_SET
.setApiGatewayConfiguration(AKSKApiGatewayConfiguration.of(AK, SK))
//Настройка билдера для HttpClient, используемого для выполнения асинхронных вызовов,
//Значение по умолчанию: NettyClientHttpConnectorBuilder
.setClientHttpConnectorBuilder(
NettyClientHttpConnectorBuilder.create()
//Настройка тайм-аута соединения для Reactor Netty HttpClient
//Переопределяет аналогичную настройку, заданную на уровне выше(DataspaceSdkApiClientConfiguration.Builder)
//Значение по умолчанию: 60_000
.connectionTimeoutMs(20_000)
//Настройка тайм-аута чтения для Reactor Netty HttpClient
//Переопределяет аналогичную настройку, заданную на уровне выше(DataspaceSdkApiClientConfiguration.Builder)
//Значение по умолчанию: 60_000
.readTimeoutMs(20_000)
//Настройка SSL для Reactor Netty HttpClient
//Переопределяет аналогичную настройку, заданную на уровне выше(DataspaceSdkApiClientConfiguration.Builder)
//Значение по умолчанию: SSLConfiguration.DEFAULT
.sslConfiguration(TRUST_ALL)
//Настройка количества потоков в EventLoop для Reactor Netty HttpClient
//Значение по умолчанию: 12
.eventLoopGroupThreadsCount(20)
//Настройки ConnectionProvider для Reactor Netty HttpClient
.connectionProvider(
ConnectionProvider.builder("customConnectionProviderName")
//...
.build()
)
)
//Настройка билдера для HttpClient, используемого для выполнения синхронных вызовов,
//Значение по умолчанию: HttpComponentsRestTemplateBuilder
//@Deprecated. Будет удален в будущих релизах
.setRestTemplateBuilder(
HttpComponentsRestTemplateBuilder.create()
//Настройка максимального количества соединений для pool коннектов Apache HttpClient
//Значение по умолчанию: 20
//Наименование системной настройки: http.maxConnections
.maxConnections(5)
//Настройка тайм-аута соединения для Apache HttpClient
//Переопределяет аналогичную настройку, заданную на уровне выше(DataspaceSdkApiClientConfiguration.Builder)
//Значение по умолчанию: 60_000
.connectionTimeoutMs(30_000)
//Настройка тайм-аута чтения для Apache HttpClient
//Переопределяет аналогичную настройку, заданную на уровне выше(DataspaceSdkApiClientConfiguration.Builder)
//Значение по умолчанию: 60_000
.readTimeoutMs(30_000)
//Настройка SSL для Apache HttpClient
//Переопределяет аналогичную настройку, заданную на уровне выше(DataspaceSdkApiClientConfiguration.Builder)
//Значение по умолчанию: SSLConfiguration.DEFAULT
.sslConfiguration(SSLConfiguration.DEFAULT)
)
//Настройка API потокового чтения
.setSearchStreamClientBuilder(
SearchStreamClientBuilder
//Адрес сервиса DataSpace и порт, на котором развернут Grpc-cервер (по умолчанию: 9000)
.createWithHost("localhost")
//или
//.createWithHostAndPort("localhost", 9000)
//или
//.createWithUrl("http://localhost:9000")
//Размер одной порции данных, отправляемой сервисом DataSpace(backpressure)
.setFetchSizeBytes(1500)
)
);
Конфигурация DataspaceCorePacketClient при использовании зависимости sdk-api-lite:
DataspaceCorePacketClient packetClient =
new DataspaceCorePacketClient(
"http://localhost:8080",
DataspaceSdkApiLiteClientConfiguration.of(builder ->
builder
//Настройка токена безопасности для всех вызовов, выполняемых данным DataSpace-клиентом
//Значение по умолчанию: SecurityInfo.NOT_SET
.setSecurityInfo(SecurityInfo.NOT_SET)
//Настройка взаимодействия с ApiGateway для всех вызовов, выполняемых данным DataSpace-клиентом
//Значение по умолчанию: ApiGatewayConfiguration.NOT_SET
.setApiGatewayConfiguration(AKSKApiGatewayConfiguration.of(AK, SK))
//Настройка билдера для HttpClient, используемого для выполнения синхронных вызовов,
//Значение по умолчанию: HttpComponentsHttpClientConfiguration.Builder
.setHttpClientConfigurationBuilder(
HttpComponentsHttpClientConfiguration.Builder.create()
//Настройка тайм-аута соединения для Apache HttpClient
//Значение по умолчанию: 60_000
.setConnectionTimeoutMs(10_000)
//Настройка тайм-аута чтения для Apache HttpClient
//Значение по умолчанию: 60_000
.setReadTimeoutMs(10_000)
//Настройка максимального количества соединений для пула коннектов Apache HttpClient
//Значение по умолчанию: 20
.setMaxConnectionsTotal(5)
//Настройка SSL для Apache HttpClient
//Значение по умолчанию: SSLConfiguration.DEFAULT
.setSSLConfiguration(TRUST_ALL)
)
)
);
Примечание
Для некоторых настроек в описании указано: «Наименование системной настройки». Это означает, что данные настройки можно изменить посредством задания системного свойства для JVM с соответствующим названием.
Конфигурация DataSpace-клиента с мониторингом#
Dataspace-клиент с WebClient может предоставлять как метрики в формате Prometheus, так и отправку Span в систему распределенной трассировки.
Важно Ваше приложение, в которое интегрируется Dataspace-клиент, уже должно быть настроено для работы со Spring Observability.
Убедитесь, что в вашем Spring Boot сервисе подключены зависимости:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
<version>${micrometer-tracing-bridge-brave.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
<scope>runtime</scope>
</dependency>
Конфигурация вашего сервиса с Dataspace-клиентом может отличаться, но в целом настройки будут выглядеть следующим образом:
spring.application.name=dataspace-client
management.endpoints.web.exposure.include=*
management.tracing.enabled=true
management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans
management.tracing.sampling.probability=1.0
Настройка клиента:
@Bean
public WebClient webClient() {
ReactorClientHttpConnector reactorClientHttpConnector =
new ReactorClientHttpConnector(new ReactorResourceFactory(),
httpClient -> httpClient.metrics(true, Function.identity())
.doOnConnected((connection -> connection.addHandlerLast(new ReadTimeoutHandler(60_000, TimeUnit.MILLISECONDS))))
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 60_000)
);
return WebClient.builder()
.baseUrl(url)
.clientConnector(reactorClientHttpConnector)
//При конфигурации WebClient по умолчанию, данный параметр равен 256KB, рекомендуется установить больше,
//чтобы избежать ошибок при чтении данных большого объема (задается в байтах)
.exchangeStrategies(ExchangeStrategies.builder().codecs(
config -> config.defaultCodecs().maxInMemorySize(1_000_000)).build()
).build();
}
В результате будут доступны метрики клиента и Reactor Netty в формате Prometheus по адресу вашего сервиса /actuator/prometheus.
Метрики клиента:
http_client_requests_seconds_count Количество запросов
http_client_requests_seconds_sum Общее время запросов. Для расчета среднего значения разделите sum / count
http_client_requests_seconds_max Максимальное время запроса
Метрики Reactor Netty:
Время отправки данных
reactor_netty_http_client_data_sent_time_seconds_max Максимальное время отправки данных во внешние сервисы (в секундах)
reactor_netty_http_client_data_sent_time_seconds_count Количество операций отправки данных
reactor_netty_http_client_data_sent_time_seconds_sum Общее время отправки данных. Для расчета среднего значения разделите sum / count
Получение данных из внешних сервисов
reactor_netty_http_client_data_received_bytes_max Максимальный объем полученных данных за один раз (в байтах)
reactor_netty_http_client_data_received_bytes_count Количество операций получения данных
reactor_netty_http_client_data_received_bytes_sum Общий объем полученных данных (в байтах)
reactor_netty_http_client_data_received_time_seconds_max Максимальное время получения данных (в секундах)
reactor_netty_http_client_data_received_time_seconds_count Число операций получения
reactor_netty_http_client_data_received_time_seconds_sum Общее время получения данных. Среднее:sum / count
Общая длительность ответа
reactor_netty_http_client_response_time_seconds_max Максимальное время обработки HTTP-запроса (в секундах)
reactor_netty_http_client_response_time_seconds_count Количество выполненных HTTP-запросов
reactor_netty_http_client_response_time_seconds_sum Общее время обработки всех запросов. Среднее: sum / count
Время установления соединения
reactor_netty_http_client_connect_time_seconds_max Максимальное время установления соединения с внешними сервисами
reactor_netty_http_client_connect_time_seconds_count Количество попыток установления соединения
reactor_netty_http_client_connect_time_seconds_sum Общее время установления соединения. Среднее:sum / count
Отправленные данные
reactor_netty_http_client_data_sent_bytes_max Максимальный объем данных, отправленных за одну операцию (в байтах)
reactor_netty_http_client_data_sent_bytes_count Количество операций отправки данных
reactor_netty_http_client_data_sent_bytes_sum Общий объем отправленных данных (в байтах)
Объем и время ожидания подключения
reactor_netty_connection_provider_pending_connections Текущее количество запросов, ожидающих соединения
reactor_netty_connection_provider_max_pending_connections Максимальное количество запросов, которые могут быть поставлены в очередь
reactor_netty_connection_provider_total_connections Общее число созданных соединений
reactor_netty_connection_provider_active_connections Количество активных соединений
reactor_netty_connection_provider_idle_connections Количество свободных (idle) соединений
Пример DashBoard для Grafana в виде json-файла Dspc_client_metrics.json можно найти в хранилище артефактов (например, Nexus) по следующему groupId, artifactId:
groupId = sbp.com.sbt.dataspace
artifactId = grafana-dashboard
Этот артефакт представляет собой jar-файл, который можно открыть zip-архиватором.
В распакованном архиве json-файл располагается по пути ./configs/grafana/prometheus/Dspc_client_metrics.json.
Конфигурация взаимодействия c ApiGateway#
Выше в разделе «Конфигурация DataSpace-клиентов» был описан параметр setApiGatewayConfiguration, который дает возможность настраивать взаимодействие DataSpace-клиентов c ApiGateway.
Для того чтобы задать данную настройку, необходимо использовать конкретную реализацию ApiGatewayConfiguration.
На данный момент доступна реализация AKSKApiGatewayConfiguration, которая позволяет задать ключи AK и SK. С их помощью будет подписан запрос, отправленный на ApiGateway.
Конфигурация SSL#
Выше в разделе «Конфигурация DataSpace-клиентов» был описан параметр setSSLConfiguration, который дает возможность настраивать SSL для HTTP-клиентов, используемых DataSpace-клиентами.
Для того чтобы задать данную настройку, необходимо использовать класс SSLConfiguration. Используя статический метод of, можно задать truststore, который будет использован при конфигурации SSL для HttpClient.
Также при настройке SSL возможно использование следующих значений:
SSLConfiguration.DEFAULT— используется по умолчанию. При настройке SSL будет использовано хранилище сертификатов, доступное на уровне ОС.SSLConfiguration.TRUST_ALL— не рекомендуется для использования в production. При использовании данного значения любой сертификат будет расцениваться как валидный. Данная настройка предназначена для целей тестирования.
Установка конфигурации на конкретный вызов API#
Следующие настройки можно задавать не только глобально на уровне конкретного DataSpace-клиента, но и при вызове конкретного API:
ApiGatewayConfiguration;SecurityInfo.
Примечание
Одновременное использование этих настроек на уровне DataSpace-клиента и передача в метод запрещены.
Для установки данных настроек «на вызов» необходимо использовать API, в параметрах которых присутствует Consumer<DataspaceApiRequestInfo.Builder> configuration.
Пример:
packetClient.execute(packet, builder ->
builder
.setApiGatewayConfiguration(AKSKApiGatewayConfiguration.of(AK, SK))
.setSecurityInfo(SecurityInfo.NOT_SET));
Передача HTTP-заголовков#
Для того чтобы добавить в запрос HTTP-заголовок, необходимо использовать API, в параметрах которых присутствует Consumer<DataspaceApiRequestInfo.Builder>.
Пример для DataspaceCorePacketClient:
packetClient.execute(packet, builder ->
builder
.addHeader("requestUid", "123e4567-e89b-12d3-a456-426655440000"));
Для того чтобы API с параметром Consumer<DataspaceApiRequestInfo.Builder> стали доступны для DataspaceCoreSearchClient, необходимо в конфигурации плагина model-api-generator-maven-plugin указать настройку:
<enableSecurityMethods>true</enableSecurityMethods>
Пример для DataspaceCoreSearchClient:
searchClient.searchProduct(with -> with.withCode(), builder ->
builder
.addHeader("requestUid", "123e4567-e89b-12d3-a456-426655440000"));
На стороне компонента DataSpace Core реализована возможность поместить значение конкретного HTTP-заголовка в Mapped Diagnostic Context(MDC).
Для этого необходимо воспользоваться настройкой dataspace.httpHeaderToMdcKeyMappings.
Данная настройка позволяет соотнести наименование HTTP-заголовка с ключом MDC.
Mapping осуществляется в следующем формате: <наименование HTTP-заголовка>:'<ключ MDC>'.
Пример: dataspace.httpHeaderToMdcKeyMappings={requestUidHeader1:'requestUidMdc1',requestUidHeader2:'requestUidMdc2'}.
Взаимодействие со Spring 5#
При использовании sdk-api в проекте Spring 5 (Spring boot 2.7) необходимо:
использовать WebClient;
определить адаптер интерфейса ClientResponse версий 6 и 5.
Пример конфигурации:
@TestConfiguration
static class SdkApiSpring6Config {
@Value("${ds.url:}")
private String dsUrl;
@Value("${ds.streamUrl:}")
private String dsStreamUrl;
@Bean
WebClient webClient() {
return WebClient.builder()
.clientConnector(NettyClientHttpConnectorBuilder.create().readTimeoutMs(500_000).build())
.exchangeStrategies(ExchangeStrategies.builder().codecs(
config -> config.defaultCodecs().maxInMemorySize(1_000_000)).build()
).build();
}
@Bean
DataspaceCorePacketClient packetClient(WebClient webClient) {
return new DataspaceCorePacketClient(
dsUrl,
DataspaceSdkApiClientConfiguration.of(
builder -> builder
.setWebClient(webClient)
.setClientResponseRawStatusCodeResolver(clientResponse -> clientResponse.rawStatusCode())
)
);
}
@Bean
DataspaceCoreSearchClient searchClient(WebClient webClient) {
return new DataspaceCoreSearchClient(
dsUrl,
DataspaceSdkApiClientConfiguration.of(builder ->
builder
.setWebClient(webClient)
.setSearchStreamClientBuilder(
SearchStreamClientBuilder.createWithUrl(dsStreamUrl)
)
.setClientResponseRawStatusCodeResolver(clientResponse -> clientResponse.rawStatusCode())
)
);
}
@Bean
DataspaceCoreHistoryClient historyClient(WebClient webClient) {
return new DataspaceCoreHistoryClient(
dsUrl,
DataspaceSdkApiClientConfiguration.of(builder -> builder
.setWebClient(webClient)
.setClientResponseRawStatusCodeResolver(clientResponse -> clientResponse.statusCode().value())
)
);
}
}
Вариант подключения зависимостей может быть таким:
<project>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>sbp.com.sbt.dataspace</groupId>
<artifactId>dataspace-parent</artifactId>
<version>${dataspace-parent.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>sbp.com.sbt.dataspace</groupId>
<artifactId>pprb-client-bom</artifactId>
<version>${pprb-client-bom.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.demo</groupId>
<artifactId>demo-model-sdk</artifactId>
<version>${demo-model-sdk.version}</version>
</dependency>
</project>
Параметры:
pprb-client-bom.version— версия зависимостей SDK изdataspace-bom;demo-model-sdk.version— версия модуля сформированных для модели артефактов;dataspace-parent.version— используется для поднятия версий зависимостей Spring, применяется версия 1.14.0.
При подключении версии 1.15.0 к архетипу версии 1.14.0 следует придерживаться следующего алгоритма действий:
В корневом pom.xml:
Изменить родителя:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent>Добавить свойства:
<resource.delimiter>${}</resource.delimiter> <pprb-client-bom.version>1.15...</pprb-client-bom.version> <dataspace-core.version>1.15...</dataspace-core.version> <dataspace-core-local-runner.version>1.15...</dataspace-core-local-runner.version> <dataspace-parent.version>1.14.1-5</dataspace-parent.version> <hibernate.version>6.2.24.Final</hibernate.version> <jakarta.persistence-api.version>3.1.0</jakarta.persistence-api.version> <jakarta.vwlidation-api.version>3.0.2</jakarta.vwlidation-api.version> <h2.version>1.4.200</h2.version>Внимание!
1.15...должно быть заменено на реальную релизную версию 1.15.0, а dataspace-parent.version на финальную релизную 1.14.0.
В pom.xml модуля -model-jpa:
Заменить следующие зависимости:
<groupId>sbp.com.sbt.dataspace</groupId> <artifactId>common-interfaces</artifactId> </dependency> <dependency> <groupId>javax.persistence</groupId> <artifactId>javax.persistence-api</artifactId> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> </dependency> <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> </dependency>На зависимости:
<dependency> <groupId>sbp.com.sbt.dataspace</groupId> <artifactId>common-interfaces</artifactId> </dependency> <dependency> <groupId>jakarta.persistence</groupId> <artifactId>jakarta.persistence-api</artifactId> <version>${jakarta.persistence-api.version}</version> </dependency> <dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> <version>${jakarta.vwlidation-api.version}</version> </dependency>В pom.xml модуля model-local-test:
Заменить следующие зависимости:
<dependency> <groupId>sbp.com.sbt.dataspace</groupId> <artifactId>dataspace-core-module</artifactId> <version>${dataspace-core.version}</version> </dependency> <dependency> <groupId>sbp.com.sbt.dataspace</groupId> <artifactId>dataspace-core-local-runner</artifactId> <scope>test</scope> </dependency>На зависимости:
<dependency> <groupId>sbp.com.sbt.dataspace</groupId> <artifactId>dataspace-core-module</artifactId> <version>${dataspace-core.version}</version> <scope>test</scope> <exclusions> <exclusion> <groupId>*</groupId> <artifactId>*</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>sbp.com.sbt.dataspace</groupId> <artifactId>dataspace-core-local-runner</artifactId> <version>${dataspace-core-local-runner.version}</version> <scope>test</scope> </dependency>В файлах local-run.sh и local-run-ext-db.sn заменить
org.springframework.boot.loader.PropertiesLauncherнаorg.springframework.boot.loader.launch.PropertiesLauncher.
Внимание!
Важно понимать, что такое подключение не рекомендуется, и его следует использовать в исключительных случаях. Альтернативным вариантом
sdk-apiявляетсяsdk-api-lite, не имеющей зависимостей на Spring.
Трассировка: интеграция со Spring Cloud Sleuth#
Spring Cloud Sleuth предоставляет механизм трассировки «из коробки».
Для интеграции со Spring Cloud Sleuth необходимо конфигурировать DataSpace-клиенты целевым способом, который описан в разделе Конфигурация DataSpace-клиентов.
Логирование клиентов SDK#
Реализовано опциональное логирование работы клиентов SDK. Логируется запрос, ответ, ошибка. Логирование пакета можно указать как для каждого клиента, так и указав общий пакет. Используется логирование slf4j.
Примеры клиента:
«sbp.sbt.sdk.DataspaceCorePacketClient» — клиент исполнения пакетов для sdk-api;
«sbp.sbt.sdk.search.BaseDataspaceCoreSearchClient» - клиент поискового api для sdk-api;
«sbp.sbt.sdk.history.BaseDataspaceCoreHistoryClient» — родительский класс поискового клиента историцирования;
«sbp.sbt.dataspace.sdk.packet.DataspaceCorePacketClient» — клиент исполнения пакетов для sdk-lite-api;
«sbp.sbt.dataspace.sdk.search.BaseDataspaceCoreSearchClient» — клиент поискового api для sdk-lite-api.
Примеры пакетов:
«sbp.sbt.dataspace.sdk» — для sdk-lite-api;
«sbp.sbt.sdk» — для sdk-api.
Пример простого logback.xml для логирования клиентов обработки пакетов и поисков из состава sdk-api:
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
</Pattern>
</layout>
</appender>
<logger name="sbp.sbt.sdk.DataspaceCorePacketClient" level="debug"/>
<logger name="sbp.sbt.sdk.search.BaseDataspaceCoreSearchClient" level="debug"/>
<root level="info">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
Метрики#
Метрики HTTP-клиента#
По умолчанию WebClient использует Reactor Netty в качестве HTTP-клиента. Данный клиент поддерживает встроенную интеграцию с Micrometer.
Подробнее о том, как настроить метрики, описано в официальной документации.
Статистика утилизации пула соединений HTTP-клиента#
Внимание!
Данная функциональность не является целевой и работает в рамках обратной совместимости.
Метод
getPoolStatsпомечен@Deprecatedи будет удален в следующих релизах.Актуальная информация по сбору метрик HTTP-клиента описана в разделе Метрики HTTP-клиента.
Примечание
В данный момент метрика доступна только при использовании зависимости sdk-api.
У каждого DataSpace-клиента доступен метод getPoolStats, возвращающий объект типа HttpPoolStats, который позволяет
получить следующую информацию:
getLeased— количество соединений, используемых в данный момент времени для выполнения запросов;getPending— количество заблокированных запросов на соединение, находящихся в ожидании свободного соединения в данный момент времени;getAvailable— количество свободных соединений в данный момент времени;getMax— максимальное количество соединений в пуле.
Пример вызова toString для объекта типа HttpPoolStats:
[leased: 7; pending: 0; available: 9; max: 20]
Гистограмма длительности вызовов#
У каждого DataSpace-клиента доступен метод getRequestsDurationHistogram, возвращающий Map<String, String>, который содержит статистику времени выполнения по всем вызовам, произведенным на текущий момент.
Пример вызова toString для вышеупомянутой Map:
{>5ms =161101, >1000ms =0, >2ms =239186, >1ms =99783, >0ms =0, >10ms =2496, >50ms =0, >100000ms =0, >700ms =0, >20ms =398, >1 0000ms =0, >100ms =0, >400ms =0}
В примере выше значения времени выполнения запросов разбиты по интервалам:
0…1ms, 1…2ms, 2…5ms, 5…10ms, 10…20ms, 20…50ms, 50…100ms, 100…400ms, 400…700ms, 700ms…1s, 1…10s, 10…100s.
Для каждого из указанных интервалов накапливается количество запросов, время выполнения которых попадает в соответствующий интервал. Выражение >5ms =161101 означает, что 161101 запросов выполнилось за время от 5 до 10 миллисекунд.
Использование асинхронного API SDK#
В рамках функциональности поискового клиента (DataspaceCoreSearchClient) а также клиента пакета команд (DataspaceCorePacketClient) предусмотрена возможность выполнения асинхронных вызовов в «реактивном» стиле c использованием класса Mono, а также в классическом стиле с использованием CompletableFuture.
Для того чтобы воспользоваться данной функциональностью, необходимо использовать метод executeAsync (в случае с SDK пакета команд) и методы с постфиксом Async (в случае с поисковым SDK).
Какая именно реализация асинхронного API будет использована определяется тем, какая зависимость была подключена:
sdk-api — содержит реализацию на основе
Mono, содержит зависимость на Spring Framework:
<dependency>
<groupId>sbp.com.sbt.dataspace</groupId>
<artifactId>sdk-api</artifactId>
</dependency>
sdk-api-lite — содержит реализацию на основе CompletableFuture, не зависит от Spring Framework:
<dependency>
<groupId>sbp.com.sbt.dataspace</groupId>
<artifactId>sdk-api-lite</artifactId>
</dependency>
Использование асинхронного API в составе SDK API#
В основе реализации асинхронного API лежит WebClient, который является частью фреймворка Spring WebFlux.
Вышеупомянутые async-методы возвращают Mono,
который параметризован типами данных, возвращаемыми их синхронными аналогами.
Mono по своей природе «ленивый». Для того чтобы вызов был произведен, требуется вызвать метод subscribe. В качестве первого параметра необходимо
передать Consumer, который обработает результат в случае успешного вызова, а в качестве второго — Consumer, который обработает ошибку, если она возникнет.
Пример вызова:
DataspaceCorePacketClient packetClient = new DataspaceCorePacketClient("http://localhost:8080");
Packet packet = new Packet();
CreateProductParam createProductParam = CreateProductParam
.create()
.setCode("code")
.setName("name");
ProductRef createProduct = packet.product.create(createProductParam);
//successResultHandler и errorHandler используются для наглядности примера и не входят в состав SDK
packetClient.executeAsync(packet)
.subscribe(
transportData -> successResultHandler.handle(transportData),
error -> errorHandler.handle(error));
Тест асинхронного вызова при использовании клиента пакета команд:
@Test
public void asyncPacketTest() throws InterruptedException {
// Создаем пакет действий.
Packet packet = new Packet();
// Добавляем в пакет действие создания сущности BookStore
String bookStoreName = "BookStoreName";
BookStoreRef bookStoreRef = packet.bookStore.create(param -> {
param.setName(bookStoreName);
});
// Тестовый класс обработки результата
class AsyncResultsHandler {
private String objectId;
private DataspaceCorePacketClient.TransportData transportData;
public void handle(BookStoreRef bookStoreRef, DataspaceCorePacketClient.TransportData transportData) {
this.objectId = bookStoreRef.getId();
this.transportData = transportData;
}
public String getObjectId() {
return this.objectId;
}
public DataspaceCorePacketClient.TransportData getTransportData() {
return transportData;
}
}
AsyncResultsHandler asyncResultsHandler = new AsyncResultsHandler();
// Выполняем пакет асинхронно
dataspaceCorePacketClient.executeAsync(packet)
.subscribe(
// Обрабатываем успешный результат
(transportData) -> asyncResultsHandler.handle(bookStoreRef, transportData),
// Если возникла ошибка — логируем ее
System.out::println);
// Имитируем полезную работу в основном потоке приложения
TimeUnit.SECONDS.sleep(5);
// Проверяем, что объект был создан — получили его Id
Assertions.assertNotNull(asyncResultsHandler.getObjectId());
Assertions.assertNotNull(asyncResultsHandler.getTransportData());
}
Тест асинхронного вызова при использовании поискового клиента:
@Test
public void asyncSearchTest() throws InterruptedException, SdkJsonRpcClientException {
// Создаем пакет действий
Packet packet = new Packet();
// Добавляем в пакет действие создания сущности BookStore
String bookStoreName = "BookStoreName";
BookStoreRef bookStoreRef = packet.bookStore.create(param -> {
param.setName(bookStoreName);
});
// Создаем BookStore при помощи синхронного метода
dataspaceCorePacketClient.execute(packet);
// Тестовый класс обработки результата
class AsyncResultsHandler {
private GraphCollection<BookStoreGet> graphCollection;
public void handle(GraphCollection<BookStoreGet> graphCollection) {
this.graphCollection = graphCollection;
}
public String getFirstBookStoreName() {
return graphCollection.get(0).getName();
}
}
AsyncResultsHandler asyncResultsHandler = new AsyncResultsHandler();
// Выполняем поиск асинхронно
dataspaceCoreSearchClient.searchBookStoreAsync(BookStoreGraph.createCollection()
.withName()
.setWhere(bookStore -> bookStore.objectIdEq(bookStoreRef.getId())))
.subscribe(
// Обрабатываем успешный результат
asyncResultsHandler::handle,
// Если возникла ошибка — логируем ее
System.out::println);
// Имитируем полезную работу в основном потоке приложения
TimeUnit.SECONDS.sleep(5);
// Проверяем, что исходный код объекта совпадает с найденным
Assertions.assertEquals(bookStoreName, asyncResultsHandler.getFirstBookStoreName());
}
Конфигурация#
WebClient требует для своей работы HTTP-клиент. Выбрать явно, какой HTTP-клиент будет использован, а также настроить его более гибко
можно, выбрав одну из реализаций ClientHttpConnectorBuilder.
В качестве HTTP-клиента по умолчанию используется Reactor Netty.
Если при инициализации DataspaceCorePacketClient или DataspaceCoreSearchClient не указывать параметр clientHttpConnectorBuilder, то по умолчанию будет использована реализация NettyClientHttpConnectorBuilder с предопределенными значениями основных параметров HTTP-клиента Reactor Netty.
Пример конфигурации клиента пакета команд при помощи NettyClientHttpConnectorBuilder:
DataspaceCorePacketClient packetClient = new DataspaceCorePacketClient(
"http://localhost:8080",
DataspaceSdkApiClientConfiguration.of(builder ->
builder.setClientHttpConnectorBuilder(
NettyClientHttpConnectorBuilder.create()
.connectionProvider(ConnectionProvider
.builder("connectionProviderName")
.maxConnections(20)
.pendingAcquireMaxCount(100)
.pendingAcquireTimeout(Duration.ofMillis(45_000))
.build())
.connectionTimeoutMs(60_000)
.readTimeoutMs(60_000)
.eventLoopGroupThreadsCount(4)
)
)
);
Также доступна возможность сконфигурировать WebClient самостоятельно:
Пример конфигурации клиента пакета команд при помощи WebClient.Builder:
DataspaceCorePacketClient packetClient = new DataspaceCorePacketClient(
"http://localhost:8080",
DataspaceSdkApiClientConfiguration.of(builder ->
builder.setWebClientBuilder(WebClient.builder()
.clientConnector(NettyClientHttpConnectorBuilder.create().build())
.exchangeStrategies(ExchangeStrategies.builder().codecs(
config -> config.defaultCodecs().maxInMemorySize(1024)).build()
)
)
)
);
Внимание!
Параметр
baseUrlустанавливать не требуется.
Использование асинхронного API в составе SDK-API-Lite#
Внимание!
DataspaceCorePacketClient и DataspaceCoreSearchClient реализуют интерфейс
AutoClosable, поэтому после завершения работы с ними необходимо явно вызвать методcloseили использовать конструкцию try-with-resources.
Вышеупомянутые async-методы возвращают CompletableFuture.
Пример вызова:
DataspaceCorePacketClient packetClient = new DataspaceCorePacketClient("http://localhost:8080");
Packet packet = new Packet();
CreateProductParam createProductParam = CreateProductParam
.create()
.setCode("code")
.setName("name");
ProductRef createProduct = packet.product.create(createProductParam);
//successResultHandler и errorHandler используются для примера и не входят в состав SDK
packetClient.executeAsync(packet)
.thenAccept(transportData -> successResultHandler.handle(createProduct, transportData))
.exceptionally(error -> {
errorHandler.handle(error);
return null;
});
//Вызываем метод close, чтобы корректно освободить используемые ресурсы (также можно использовать try-with-resources)
packetClient.close();
Примечание
Для запуска теста необходимо перед сборкой model-sdk заменить зависимость sdk-api на sdk-api-lite, а также в BaseTest.java необходимо заменить импорт
import sbp.sbt.sdk.DataspaceCorePacketClientнаimport sbp.sbt.dataspace.sdk.packet.DataspaceCorePacketClient.
Тест асинхронного вызова при использовании клиента пакета команд:
@Test
public void asyncPacketTest() throws InterruptedException {
// Создаем пакет действий
Packet packet = new Packet();
// Добавляем в пакет действие создания сущности BookStore
String bookStoreName = "BookStoreName";
BookStoreRef bookStoreRef = packet.bookStore.create(param -> {
param.setName(bookStoreName);
});
// Тестовый класс обработки результата
class AsyncResultsHandler {
private String objectId;
private DataspaceCorePacketClient.TransportData transportData;
public void handle(BookStoreRef bookStoreRef, DataspaceCorePacketClient.TransportData transportData) {
this.objectId = bookStoreRef.getId();
this.transportData = transportData;
}
public String getObjectId() {
return this.objectId;
}
public DataspaceCorePacketClient.TransportData getTransportData() {
return transportData;
}
}
AsyncResultsHandler asyncResultsHandler = new AsyncResultsHandler();
// Выполняем пакет асинхронно
dataspaceCorePacketClient.executeAsync(packet)
// Обрабатываем успешный результат
.thenAccept(transportData -> asyncResultsHandler.handle(bookStoreRef, transportData))
// Если возникла ошибка — логируем ее
.exceptionally(error -> {
System.out.println(error.getMessage());
return null;
});
// Имитируем полезную работу в основном потоке приложения
TimeUnit.SECONDS.sleep(5);
// Проверяем что объект был создан — получили его Id
Assertions.assertNotNull(asyncResultsHandler.getObjectId());
Assertions.assertNotNull(asyncResultsHandler.getTransportData());
}
Примечание
Для запуска теста необходимо перед сборкой model-sdk заменить зависимость sdk-api на sdk-api-lite, а также в BaseTest.java необходимо заменить импорт
import sbp.sbt.sdk.DataspaceCorePacketClientнаimport sbp.sbt.dataspace.sdk.packet.DataspaceCorePacketClient.
Тест асинхронного при использовании поискового клиента:
@Test
public void asyncSearchTest() throws InterruptedException, SdkJsonRpcClientException {
// Создаем пакет действий
Packet packet = new Packet();
// Добавляем в пакет действие создания сущности BookStore
String bookStoreName = "BookStoreName";
BookStoreRef bookStoreRef = packet.bookStore.create(param -> {
param.setName(bookStoreName);
});
// Создаем BookStore при помощи синхронного метода
dataspaceCorePacketClient.execute(packet);
// Тестовый класс обработки результата
class AsyncResultsHandler {
private GraphCollection<BookStoreGet> graphCollection;
public void handle(GraphCollection<BookStoreGet> graphCollection) {
this.graphCollection = graphCollection;
}
public String getFirstBookStoreName() {
return graphCollection.get(0).getName();
}
}
AsyncResultsHandler asyncResultsHandler = new AsyncResultsHandler();
System.out.println();
// Выполняем поиск асинхронно
dataspaceCoreSearchClient.searchBookStoreAsync(BookStoreGraph.createCollection()
.withName()
.setWhere(bookStore -> bookStore.objectIdEq(bookStoreRef.getId())))
// Обрабатываем успешный результат
.thenAccept(asyncResultsHandler::handle)
// Если возникла ошибка — логируем ее
.exceptionally(error -> {
System.out.println(error.getMessage());
return null;
});
// Имитируем полезную работу в основном потоке приложения
TimeUnit.SECONDS.sleep(5);
// Проверяем, что исходный код объекта совпадает с найденным
Assertions.assertEquals(bookStoreName, asyncResultsHandler.getFirstBookStoreName());
}
Конфигурация#
Каждый из DataSpace-клиентов из состава DataSpace Java SDK требует для своей работы HTTP-клиент. Выбрать явно, какой HTTP-клиент будет использован, а также настроить его более гибко можно, выбрав одну из реализаций HttpClientConfiguration.
В данный момент доступна следующая реализация HttpClientConfiguration: HttpComponentsHttpClientConfiguration — конфигурирует в качестве HTTP-клиента HttpClient от Apache.
В качестве HTTP-клиента по умолчанию используется HttpClient от Apache.
Если при инициализации DataspaceCorePacketClient или DataspaceCoreSearchClient не указывать параметр configuration, то по умолчанию будет использована реализация HttpComponentsHttpClientConfiguration с предопределенными значениями основных параметров HTTP-клиента HttpClient.
Пример конфигурации SDK пакета команд при помощи HttpComponentsHttpClientConfiguration:
DataspaceCorePacketClient packetClient = new DataspaceCorePacketClient(
"http://localhost:8080",
DataspaceSdkApiLiteClientConfiguration.of(builder ->
builder.setHttpClientConfigurationBuilder(
HttpComponentsHttpClientConfiguration.Builder.create()
.setConnectionTimeoutMs(60_000)
.setReadTimeoutMs(60_000)
.setMaxConnectionsTotal(20)
)
)
);
Использование API потокового чтения#
В рамках функциональности поискового клиента (DataspaceCoreSearchClient) предусмотрена возможность получения данных в виде потока в «реактивном» стиле c использованием класса Flux.
Для того чтобы воспользоваться данной функциональностью, необходимо использовать методы с постфиксом Stream.
Данная функциональность доступна только при использовании зависимости sdk-api:
<dependency>
<groupId>sbp.com.sbt.dataspace</groupId>
<artifactId>sdk-api</artifactId>
</dependency>
Вышеупомянутые async-методы возвращают Flux вместо GraphCollection.
Flux по своей природе «ленивый». Для того чтобы вызов был произведен, требуется вызвать метод subscribe.
Более подробно о том, как работать с Flux, описано в официальной документации Project Reactor.
В случае возникновения ошибки чтение прерывается.
Внимание!
При использовании API потокового чтения отсутствует возможность получать в ответе любые коллекции. При этом в запросе сохраняется возможность передать спецификацию, содержащую запрос коллекционных полей, т.к. механизм построения спецификаций является универсальным для всех поисковых API, но при попытке вызова методов получения коллекций на стороне DataSpace Java SDK будет выброшено исключение
UnsupportedOperationException.
Пример вызова:
DataspaceCoreSearchClient searchClient = new DataspaceCoreSearchClient(
"http://localhost:8080",
DataspaceSdkApiClientConfiguration.of(
builder -> builder.setSearchStreamClientBuilder(
SearchStreamClientBuilder.createWithUrl("http://localhost:9000")
)
)
);
ProductCollectionWith<? extends ProductGrasp> productSearchSpec =
ProductGraph.createCollection()
.withCode()
.withName()
.setWhere(where -> where.codeEq("ProductCode"));
//successResultHandler и errorHandler используются для наглядности примера и не входят в состав SDK
searchClient.searchProductStream(productSearchSpec)
.subscribe(
productGet -> successResultHandler.handle(productGet),
error -> errorHandler.handle(error));
Конфигурация на стороне сервиса DataSpace#
Функциональность потокового чтения основана на протоколе gRPC. Соответственно, на стороне сервиса DataSpace стартует отдельный gRPC-сервер. Вследствие этого данная функциональность на стороне сервиса DataSpace поставляется по умолчанию в выключенном виде. Это позволит экономить ресурсы тех потребителей, которым потоковое чтение данных не требуется.
Для того чтобы включить функциональность потокового чтения, необходимо установить для настройки dataspace.stream.grpcEnable сервиса DataSpace значение true.
Настройка dataspace.stream.grpcServerPort позволяет сконфигурировать порт, на котором будет запущен gRPC-сервер(по умолчанию: 9000).
Во избежание исчерпания ресурсов БД и влияния на транзакционную активность число одновременных запросов к реактивному API ограничено настройкой dataspace.stream.requestsLimit. Значение по умолчанию: 2. Если количество одновременных запросов превысит значение данной настройки, то на клиента будет отправлена ошибка Too many requests.
Конфигурация на стороне клиентского приложения#
При инициализации экземпляра DataspaceCoreSearchClient необходимо задать параметр SearchStreamClientBuilder. При этом необходимо указать адрес хоста, на котором развернут сервис DataSpace.
Также присутствует возможность задать порт, на котором запущен gRPC-сервер (по умолчанию: 9000), либо использовать url для настройки.
Доступна возможность конфигурации порции данных в байтах (setFetchSizeBytes), которую сервис DataSpace будет отправлять в ответ по мере того, как клиент будет обрабатывать полученные ответы на своей стороне (backpressure).
DataspaceCoreSearchClient searchClient = new DataspaceCoreSearchClient(
"http://localhost:8080",
DataspaceSdkApiClientConfiguration.of(
builder -> builder.setSearchStreamClientBuilder(
SearchStreamClientBuilder
.createWithHost("localhost")
//или
//.createWithHostAndPort("localhost", 9000)
//или
//.createWithUrl("http://localhost:9000")
.setFetchSizeBytes(1500)
)
)
);
Дополнительные методы SearchStreamClientBuilder:
createWitTarget(String target): дополнительный конструкторSearchStreamClientBuilderс созданием экземпляраNettyChannelBuilderметодомforTarget. Пример значенияtarget:dns:///example.com;createWithCustomNettyChannelBuilder(Supplier<NettyChannelBuilder> supplier): дополнительный конструкторSearchStreamClientBuilder, в котором экземплярNettyChannelBuilderполностью предоставляется потребителем;addHeader(String key, String value): добавляет header к вызовуgrpcс ключомkeyи значениемvalue. ДляSearchStreamClientBuilderможет вызываться столько раз, сколько header требуется добавить с различающимися ключами. Указание header может быть использовано для маршрутизации;nettyChannelBuilderCustomize(Consumer<NettyChannelBuilder> nettyChannelBuilderConsumer): метод доступа к экземпляруNettyChannelBuilderдля кастомизации;setReactorSearchServiceStubCustomizer(UnaryOperator<ReactorSearchServiceStub> reactorSearchServiceStubCustomizer): метод кастомизации экземпляраReactorSearchServiceStub, созданного для канала на основеNettyChannelBuilder.
Конфигурация для запуска тестов#
Для запуска теста необходимо в конфигурационном файле dataspace-core-local-runner.properties дополнить настройку overridden-spring-properties значением --dataspace.stream.grpcEnable=true.
В BaseTest.java необходимо провести дополнительную настройку экземпляра DataspaceCoreSearchClient следующим образом:
protected static DataspaceCoreSearchClient dataspaceCoreSearchClient =
new DataspaceCoreSearchClient("http://localhost:" + servicePort,
DataspaceSdkApiClientConfiguration.of(
builder -> builder.setSearchStreamClientBuilder(
SearchStreamClientBuilder
.createWithUrl("http://localhost:9000")
)
));
В файл pom.xml модуля model-local-test необходимо добавить зависимость:
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
Пример теста:
@Test
public void searchStreamTest() throws SdkJsonRpcClientException {
String bookStoreName = "BookStoreName";
//Создаем 10 книжных магазинов
for (int i = 0; i < 10; i++) {
Packet packet = new Packet();
packet.bookStore.create(param -> param.setName(bookStoreName));
dataspaceCorePacketClient.execute(packet);
}
//Вызываем API потокового чтения
Flux<BookStoreGet> bookStoreFlux = dataspaceCoreSearchClient
.searchBookStoreStream(with -> with
.withName()
.setWhere(where -> where.nameEq(bookStoreName)));
StepVerifier.create(
bookStoreFlux
//Печатаем имя каждого магазина в консоль
.doOnNext(bookStore -> System.out.println(bookStore.getName()))
)
//Ожидаем, что вычитаем 10 сущностей
.expectNextCount(10)
.verifyComplete();
}
Использование идемпотентности (Idempotency)#
Идемпотентность — свойство системы, благодаря которому повторный идентичный вызов, сделанный один или несколько раз подряд, не изменяет состояние системы. Применительно к DataSpace повторение вызова в рамках одного пакета не изменяет состояние агрегата. При этом:
В DataSpace идемпотентность доступна «из коробки» при вызове пакета команд.
Чтобы воспользоваться идемпотентностью, при создании пакета Unit of Work нужно передать параметр
idempotencePacketId.Состав пакета Unit of Work в части команд и их параметров при повторных вызовах с одним и тем же значением параметра
idempotencePacketIdдолжен быть одинаковым. В противном случае возникнет ошибка.
Примечание
При использовании DataSpace Java SDK все ошибки, связанные с идемпотентностью, имеют тип
sbp.sbt.sdk.exception.detailedexception.IdempotencyException.
Алгоритм работы компонента#
На основе параметра idempotencePacketId, установленного в пакете Unit of Work, производится запрос в системную таблицу.
Результат работы алгоритма зависит от наличия или отсутствия искомой записи с заданным idempotencePacketId в системной таблице.
Если искомая запись не найдена в системной таблице, то механизм выполнит следующие действия:
Произведет реальное выполнение команд пакета.
Сериализует результат выполнения команд пакета и хеш параметров команд и сохранит данные в системную таблицу (вместе с ключом идемпотентности).
Произведет commit транзакции и вернет клиенту результат.
Примечание
В вычислении хеша участвуют параметры, которые были явно были использованы в параметрах вызова
packet.
Если в системной таблице найдена запись, то механизм сравнит хеш параметров команд текущего вызова с хешем параметров команд из сохраненной ранее записи. При этом возможны два случая:
Хеш совпали. Реального выполнения команд пакета не происходит. Клиенту возвращается сохраненный ранее результат, происходит commit транзакции.
Хеш не совпали. Возникает исключение, которое сообщает о невозможности осуществления идемпотентного вызова, происходит rollback транзакции.
Внимание!
По отношению к ключу идемпотентности используется ограничение: уникальный в пределах таблицы.
Предусмотрена возможность исключения отдельных атрибутов сущности из расчета проверочного хеш.
В модели данных для конкретной сущности в блоке <idempotence-exclude> необходимо указать наименования тех атрибутов, которые требуется исключить.
Пример:
<class name="IdempotenceExcludeTest" label="сущность со списком свойств в секции idempotence-exclude">
<property name="prop1" type="String"/>
<property name="prop2" type="String"/>
<property name="prop3" type="String"/>
<idempotence-exclude>
<property name="prop1"/>
<property name="prop3"/>
</idempotence-exclude>
</class>
Примеры использования идемпотентности#
Примеры:
@Test
public void idempotenceTest() throws Throwable {
String idempotenceKey = UUID.randomUUID().toString();
CreateBookStoreParam createCreateBookStoreParam = CreateBookStoreParam.create()
.setName("BookStoreName")
.setAddress("Address");
// Создаем пакет действий. При этом задаем ключ идемпотентности
Packet firstCallPacket = Packet.createIdempotencyPacket(idempotenceKey);
BookStoreRef firstCallCreateBookStore = firstCallPacket.bookStore.create(createCreateBookStoreParam);
// Выполняем пакет
dataspaceCorePacketClient.execute(firstCallPacket);
// Первый вызов пакета имеет признак не идемпотентного ответа
Assertions.assertFalse(firstCallPacket.isIdempotenceResponse());
//Запоминаем идентификатор созданной сущности
String createProductPartyId = firstCallCreateBookStore.getId();
//Снова выполняем пакет
Packet secondCallPacket = Packet.createIdempotencyPacket(idempotenceKey);
BookStoreRef secondCallCreateBookStore = secondCallPacket.bookStore.create(createCreateBookStoreParam);
dataspaceCorePacketClient.execute(secondCallPacket);
//Проверяем, что идентификаторы совпадают, значит второй раз создания новой сущности не произошло
Assertions.assertEquals(createProductPartyId, secondCallCreateBookStore.getId());
// Второй вызов пакета имеет признак идемпотентного ответа
Assertions.assertTrue(secondCallPacket.isIdempotenceResponse());
}
@Test
public void idempotenceParamsHashFailedTest() throws Throwable {
String idempotenceKey = UUID.randomUUID().toString();
CreateBookStoreParam createCreateBookStoreParam = CreateBookStoreParam.create()
.setName("BookStoreName")
.setAddress("Address");
// Создаем пакет действий. При этом задаем ключ идемпотентности
Packet firstCallPacket = Packet.createIdempotencyPacket(idempotenceKey);
firstCallPacket.bookStore.create(createCreateBookStoreParam);
// Выполняем пакет
dataspaceCorePacketClient.execute(firstCallPacket);
//Меняем параметр команды создания сущности
CreateBookStoreParam createCreateBookStoreParamNew = CreateBookStoreParam.create()
.setName("BookStoreNameNew")
.setAddress("Address");
//Снова выполняем пакет, задаем тот же самый ключ идемпотентности, что и для первого пакета
Packet secondCallPacket = Packet.createIdempotencyPacket(idempotenceKey);
secondCallPacket.bookStore.create(createCreateBookStoreParamNew);
//Проверяем, что возникло исключение sbp.sbt.sdk.exception.detailedexception.IdempotencyException, а также текст сообщения о расхождении в параметрах
IdempotencyException idempotencyException = Assertions.assertThrows(IdempotencyException.class, () -> dataspaceCorePacketClient.execute(secondCallPacket));
Assertions.assertTrue(
idempotencyException.getMessage().contains("При попытке получить сохраненный ранее результат вызова команды c id = 0 " +
"из пакета с idempotencePacketId = " + idempotenceKey + " выявлено расхождение " +
"входящих параметров (хеш входных параметров текущего вызова не совпадает с хеш сохраненного вызова). " +
"Необходимо либо использовать idempotencePacketId предыдущего вызова, либо убрать расхождение во входных параметрах.")
);
}
Использование оптимистических блокировок в пакетах команд#
Оптимистическая блокировка агрегата позволяет выстраивать шаги сценария (пакеты команд) в цепочки и гарантировать, что между шагами сценария не появится внешняя транзакционная нагрузка. В противном случае возникнет ошибка AggregateVersionException и сценарий может быть исполнен заново.

Ключевые положения#
Единицей версионирования в концепции DataSpace является агрегат, который автоматически определяется для каждого транзакционного пакета. При этом в каждой транзакции допускается работа только с одним агрегатом (см. описание в справочном разделе «DDD-Агрегаты» документа «Руководство по ведению модели данных»).
Версия агрегата увеличивается на 1 после каждого изменения агрегата. Для этого не требуется каких-либо настроек или флагов.
Однако результирующая версия скрыта по умолчанию. Чтобы получить результирующую версию, необходимо сформировать пакет с соответствующим флагом.
Этот же флаг позволяет передать полученную от предыдущего пакета версию на вход для валидации. В случае несовпадения переданной версии транзакция откатывается и возвращается ошибка AggregateVersionException (см. примеры ниже).
На базе версионирования агрегата также работает механизм версионирования векторов изменений, реплицируемых через прикладной журнал. Данный механизм гарантирует корректный порядок применения векторов.
Взаимодействие с идемпотентностью#
В случае, если для пакета с указанием ключа идемпотентности обнаруживается, что он уже исполнялся (идемпотентный вызов) оптимистическая блокировка ведет себя следующим образом:
Версия, переданная на вход, игнорируется, то есть валидация не выполняется.
На выходе возвращается актуальная версия на момент конца транзакции.
Получение версии в пакетах Read-Only#
В пакетах, содержащих только операции чтения (read-only), при запросе версии возвращается версия агрегата, считанная атомарно в первом чтении пакета (то есть самая ранняя версия в пакете).
Версию на вход в такой пакет передать нельзя.
Рекомендуется в качестве первого пакета (с запросом версии и без версии на входе) в цепочке шагов сценария, связанных оптимистической блокировкой, выбирать пакет содержащий только операции чтения.
Примеры использования#
Рассмотрим примеры использования пакетов следующих типов:
пакет без входной версии с получением результирующей версии:
final Packet packet = Packet.builder().withAggregateVersionRequest().build(); final ProductPartyRef productPartyRef = packet.productParty.create(CreateProductPartyParam.create()); assertThatCode(() -> dataspaceCorePacketClient().execute(packet)).doesNotThrowAnyException(); final Long aggregateVersionAfterCreate = packet.getAggregateVersion();пакет с входной версией и получением результирующей версии:
final Packet updateProductPacket = Packet.builder().withAggregateVersion(aggregateVersionAfterCreate).build(); final String productName = uuid(); updateProductPacket.productParty.update( productPartyRef, p -> p.setName(productName) ); assertThatCode(() -> dataspaceCorePacketClient().execute(updateProductPacket)).doesNotThrowAnyException(); final Long aggregateVersionAfterUpdate = updateProductPacket.getAggregateVersion();пакет с устаревшей входной версией:
try { Packet updateProductPacket = Packet.builder().withAggregateVersion(1L).build(); final String productName = uuid(); updateProductPacket.productParty.update( productPartyRef, p -> p.setName(productName)); dataspaceCorePacketClient().execute(updateProductPacket); } catch (AggregateVersionException e) { // Version 1 required but 2 found // handle exception }пакет только на чтение с получением версии:
Packet readProductPacket = Packet.builder().withAggregateVersionRequest().build(); ProductPartyGet productPartyGet = readProductPacket.productParty.get( productPartyRef, ProductPartyWith::withName ); assertThatCode(() -> dataspaceCorePacketClient().execute(readProductPacket)).doesNotThrowAnyException(); final Long aggregateVersionAfterRead = readProductPacket.getAggregateVersion();
Использование прикладных блокировок ресурсов#
Прикладная блокировка представляет собой реализацию механизма пессимистического блокирования ресурсов в распределенной среде Прикладная блокировка ставится на определенное время и позволяет организовать последовательное выполнение изменений данных.
Внимание!
Для корректной работы механизма прикладной блокировки необходимо обеспечить синхронность системного времени на серверах приложений, на которых развернуты модули DataSpace, и на серверах БД.
Внимание!
Прикладная блокировка не блокирует сами данные (экземпляр агрегата и его дочерние элементы) от изменения в других параллельных транзакциях. Она лишь обеспечивает выставление флага блокировки для синхронизации и эксклюзивного доступа к ресурсу посредством конструкций
tryLock/unlock.
Включение функциональности прикладной блокировки для класса модели происходит путем установки атрибута lockable="true".
Данный атрибут можно выставить только на базовых классах модели.
Пример разметки model.xml для включения механизма прикладной блокировки:
<class name="Deposit" extends="Product" label="Депозит клиента" lockable="true">
<property name="rateHistoryList" type="ProductRate" collection="set" mappedBy="deposit"
label="История процентных ставок по депозиту"/>
<property name="lastCptDate" type="Date" label="Дата последний капитализации"/>
<property name="declaration" type="String" label="Описание (комментарий)"/>
<property name="sum" type="BigDecimal" label="Сумма"/>
<property name="minBalance" type="BigDecimal" label="Неснижаемый остаток"/>
<property name="depositCurrency" type="Currency" label="Валюта депозита"/>
<property name="depositKind" type="DepositProduct" label="Вид депозита"/>
<property name="depositAccount" type="DepositAccount" mappedBy="deposit" label="Счет депозита"/>
</class>
Правила работы с клиентским API#
Синхронизация доступа к объектам в распределенной нетранзакционной среде осуществляется посредством вызова специальных команд пакета:
tryLock— попытка установки блокировки;unlock— снятия блокировки.
Использование клиентского API прикладных блокировок показано на примере следующего кода:
String initialDepositCode = "initialDepositCode";
String updateDepositCode = "updateDepositCode";
String productNum = "Б-29015";
String lockReason = "Установка блокировки сервисом N";
Long lockTimeout = 5000L;
// пакет для создания депозита
final Packet packet = Packet.createPacket();
DepositRef depositRef = packet.deposit.create(param -> {
param.setCode(initialDepositCode);
param.setNum(productNum);
});
// установка блокировки
LockRs tryLockRs = packet.deposit.tryLock(depositRef, lockRq -> {
lockRq.setTimeout(lockTimeout);
lockRq.setReason(lockReason);
});
assertThatCode(() -> dataspaceCorePacketClient.execute(packet)).doesNotThrowAnyException();
// значение сгенерированного на стороне сервера токена блокировки
String lockToken = tryLockRs.getToken();
// создание пакета для изменения депозита и снятия блокировки
Packet updatePacket = new Packet();
updatePacket.deposit.update(depositRef, param -> {
param.setCode(updateDepositCode);
});
// снятие блокировки
updatePacket.deposit.unlock(depositRef, lockToken);
assertThatCode(() -> dataspaceCorePacketClient.execute(updatePacket)).doesNotThrowAnyException();
Алгоритмы и принципы работы прикладных блокировок#
В текущей реализации логика прикладных блокировок tryLock представлена на блок-схеме:

Атрибутный состав входящих параметров команды tryLock
setTimeout(Long timeout)— период действия блокировки с момента установки (мс);setToken(String token)— токен блокировки (при первоначальной установке блокировки токен генерируется сервисом, при продлении блокировки необходимо передать ранее выданный токен);setReason(String reason)— причина установки блокировки; является опциональным.
Настройка времени ожидания для forUpdate:
waitTime(Integer)— задается через настройку dataspace.applock.waitTime (мс) в зависимости от СУБД свойство может не поддерживаться (например h2 не поддерживает) По умолчанию значение не задано, что эквивалентно NOWAIT
Результат выполнения команды tryLock:
String getToken()— сгенерированное сервером значение токенаLong getTimeoutEndTime()— время истечения блокировки
В текущей реализации логика прикладных блокировок unlock представлена на блок-схеме:

Входящие параметры вызова команды unlock:
id— идентификатор заблокированной сущности, включая ссылку на результат другой команды create (ref:…)appLockToken— токен блокировки, полученный в результате успешного выполнения команды tryLock
Сигнатура команды unlock на примере Deposit:
LockRs unlock(DepositRef deposit, String appLockToken)
Значением параметра appLockToken должен быть токен блокировки, полученный в результате успешного выполнения команды tryLock.
Для команд tryLock и unlock возможно совмещение в одном пакете. Пример демонстрирует использование токена:
final DepositRef depositRef = packet.deposit.create(CreateDepositParam.create());
final LockRs lockRs = packet.deposit.tryLock(depositRef, lockRq -> lockRq.setTimeout(5_000L));
packet.deposit.unlock(depositRef, lockRs.getToken());
assertThatCode(() -> dataspaceCorePacketClient.execute(packet)).doesNotThrowAnyException();
При ошибочном выполнении команд tryLock и unlock пакет завершается ошибкой с генерацией исключения ApplicationLockException.
Следующий пример демонстрирует генерацию ошибки использования токена:
final Packet packet = Packet.createPacket();
final DepositRef depositRef = packet.deposit.create(CreateDepositParam.create());
packet.deposit.tryLock(depositRef, lockRq -> {
lockRq.setToken("SAMPLE");
lockRq.setTimeout(5_000L);
});
assertThatCode(() -> dataspaceCorePacketClient.execute(packet))
.isInstanceOf(ApplicationLockException.class)
.hasMessage("Ошибка обработки команды id = '1', name = 'tryLock': Ошибка установки прикладной " +
"блокировки Deposit (id=7046013306958446593). " +
"Первоначальный захват блокировки возможен только с автогенерацией токена!");
Сценарии работы с прикладными блокировками#
Существует два сценария работы с прикладными блокировками. Данные сценарии показаны на схемах:

Поиск заблокированных и не заблокированных данных#
Внимание!
Корректность поиска заблокированных/не заблокированных данных гарантируется только после полного перехода прикладных блокировок на использование нового поля
syalUnlockTime.
Пример построения поискового запроса не заблокированных данных:
PerformedServiceCollectionWith<? extends PerformedServiceGrasp> req = GraphCreator.selectPerformedService()
.withCode()
.withName()
.setWhere(where -> where.syalUnlockTime().isNull().or(where.syalUnlockTime().less(GraspHelper.now())));
В примере GraspHelper.now() — преобразуется к получению текущего времени БД.
Примечание
Рекомендуется к приведенному условию фильтрации добавить дополнительные критерии фильтрации, повышающие селективность поиска.
Пример построения поискового запроса заблокированных данных:
PerformedServiceCollectionWith<? extends PerformedServiceGrasp> req = GraphCreator.selectPerformedService()
.withCode()
.withName()
.setWhere(where -> where.syalUnlockTime().greaterOrEq(GraspHelper.now()));
Ограничение максимального времени выполнения SQL-запросов в БД#
В сервисе DataSpace Core реализована возможность ограничивать максимальное время выполнения запросов в БД.
Ограничение устанавливается на общее время всех SQL-запросов, выполняемых в ходе обработки запроса к API DataSpace. При достижении ограничения выбрасывается соответствующее исключение и обработка запроса к API DataSpace завершается ошибкой, сессия с БД прерывается, а сделанные в транзакции БД изменения откатываются.
Ограничение может быть установлено по умолчанию для всех запросов или индивидуально для конкретного зароса к API DataSpace.
Примечание
Возможность доступна только при работе с СУБД, являющейся одним из вариантов PostgreSQL, поскольку используется специфика этой СУБД.
Примечание
Ограничение применимо только для GraphQL API /graphql (query и mutation) и JSON-RPC API /search и /packet. Оно также отключается при использовании механизма буферизации пакетов команд.
Задание для ограничения максимального времени выполнения SQL-запросов в БД значения по умолчанию#
Для установки ограничения для всех операций сервиса Dataspace Core необходимо задать настройки:
dataspace.defaultSqlQueryLimitTimeSec=60(по умолчанию 0 - ограничения отсутствуют). Значение настройки - целое число секунд от 0 до 86400 (1 сутки). Значение 0 означает отсутствие ограничений.
dataspace.sqlQueryLimitTimeoutRefreshPeriodSec=2(по умолчанию 1 секунда). Значение настройки - целое число секунд от 0 до значения dataspace.defaultSqlQueryLimitTimeSec. Параметр регулирует гранулярность проверки достижения ограничения при выполнении пакета команд.
Задание ограничения максимального времени выполнения SQL-запросов в БД для конкретного вызова API DataSpace#
Для установки ограничения для конкретного запроса к API необходимо передать значение ограничения в заголовке x-dspc-sqlquerylimittimesec.
Значение заголовка - целое число секунд от 0 до 86400 (1 сутки). Значение 0 означает снятие ограничений.
Пример: x-dspc-sqlquerylimittimesec=10 устанавливает ограничение в 10 секунд.
Значение ограничения для конкретного запроса имеет приоритет над значением, заданным по умолчанию.
Использование механизма буферизации пакетов команд#
В сервисе DataSpace Core реализована возможность оптимизации работы с БД путем группировки запросов Packet в одну транзакцию БД
Внимание!
Буферизация имеет смысл, если вероятность отката транзакции с пачкой запросов по причине ошибок одного из запросов стремится к нулю, в противном случае вместо увеличения производительности будет снижение вследствие повторов.
Целесообразность использования буферизации необходимо подтверждать нагрузочными тестами в конкретных сценариях потребителя DataSpace.
По умолчанию опция буферизации пакетов команд выключена. Для того чтобы ее использовать, необходимо:
установить на стороне сервиса DataSpace Core значение для настройки
dataspace.buffering.workers-countбольше «0».использовать при формировании запроса параметр
enableBuffering— признак допустимости его буферизации. Признак должен быть явно установлен в «false» для тех запросов, для которых возможны ошибки при обработке запроса в составе пачки.
Ограничения#
Пакеты, не подлежащие буферизации:
использующие оптимистическую блокировку;
использующие операцию
compare;использующие операцию
updateOrCreate;состоящие из одних
get-команд;имеющие в своем составе операции над историцируемыми сущностями.
Попытка установить для таких пакетов параметр enabledBuffering=true не возымеет эффекта, запросы будут обработаны в
штатном режиме.
Также буферизация будет отключена в для GraphQL-запроса, в котором содержится мутация, состоящая из нескольких запросов Packet. При обработке такой мутации все входящие в нее запросы Packet будут обрабатываться последовательно, буферизация при этом существенно замедлит выполнения всего запроса. Пример такого GraphQL-запроса:
mutation {
p1: packet(enableBuffering: true) {
p: createProduct(input: {
code: "123", name:"456"
}) {
id
code
name
}
}
p2: packet(enableBuffering: true) {
p: createProduct(input: {
code: "123", name:"456"
}) {
id
code
name
}
}
p3: packet(enableBuffering: true) {
p: createProduct(input: {
code: "123", name:"456"
}) {
id
code
name
}
}
}
Несмотря на то, что установлен параметр enableBuffering: true, буферизация не будет применена ни для одного из packets.
Примечание
При использовании механизма буферизации пакетов команд не используется ограничение максимального времени выполнения запросов к БД.
Алгоритм работы#
Алгоритм работы следующий:
Клиент штатным образом направляет в DataSpace Core запрос с пакетом и ждет ответа.
Контроллер DataSpace Core выполняет предварительную обработку запроса (пакета), в том числе определяет, допустима ли для него буферизация.
Если буферизация недопустима — обрабатывает пакет обычным образом (as is) и возвращает ответ на запрос.
Если для пакета допустима буферизация — помещает пакет в очередь и ожидает его обработки. Под пакетом здесь и далее понимается сам пакет команд UnitOfWork и его контекст-запрос.
В случае успешной обработки — возвращает ответ на запрос.
При получении ошибки — обрабатывает пакет обычным образом (as is) в самостоятельной транзакции и возвращает ответ на запрос.
Параллельно обработчик буфера забирает очередной пакет из очереди, открывает транзакцию БД (если не открыта), осуществляет подготовку изменений в БД и останавливается перед выполнением логики StandIn-блокировки и отправки векторов изменений.
Если время выполнения обработки транзакции не превышает заданной величины — забирает из очереди очередной пакет и выполняет его обработку.
Если время выполнения обработки транзакции превысило заданную величину или достигнут максимальный размер буфера или в очереди отсутствуют пакеты (чтобы не задерживать) — выполняется логика StandIn-блокировок, формирование векторов изменений (аналогично мульти-агрегатному пакету), их отправка и commit транзакции в БД.
В случае успешного commit возвращается успешный ответ по каждому пакету (запросу), вошедшему в транзакцию.
Примечание
В случае ошибки при обработке пачки выполняется повторная обработка каждого включенного в пачку (транзакцию) пакета уже отдельными самостоятельными транзакциями.
Триггером завершения обработки пачки может являться выполнение одного из условий:
превышение заданного времени выполнения транзакции;
достижение максимального числа пакетов в транзакции;
отсутствие пакетов в очереди (означает, что нагрузка невысокая и лучше отдать быстрее результат, чем ожидать отсечки по времени).
Примечание
Взаимодействие с Mapped Diagnostic Context (MDC)
Обработка буферизированных запросов происходит в отдельном потоке, поэтому значения одинаковых ключей MDC из исходных потоков конкатенируются.
Формирование запросов#
При формировании запросов Packet необходимо использовать параметр enableBuffering (true/false).
Если параметр не задан, будет использовано значение настройки dataspace.buffering.enable-by-default на стороне сервиса DataSpace Core, которое по умолчанию равно «false». То есть по умолчанию запросы, для которых не указан параметр enableBuffering не будут буферизованы.
Пример формирования JsonRPC-запроса с использованием буферизации:
{
"jsonrpc": "2.0",
"id": "1",
"method": "execute",
"params": {
"packet": {
"enableBuffering": true,
"commands": [
{
"id": "0",
"name": "create",
"params": {
"type": "Product",
"code": "b3366181-deee-4656-9d6b-f58fc1a1fd8d"
}
}
]
}
}
}
Пример формирования JsonRPC-запроса с использованием буферизации при помощи DataSpace Java SDK:
Packet packet = Packet.createPacketWithBuffering(true);
// или
Packet packet = Packet.builder().withBuffering(true).build();
packet.product.create(param -> param.setCode(UUID.randomUUID().toString()));
dataspaceCorePacketClient().execute(packet);
Пример формирования GraphQL-запроса с использованием буферизации:
mutation {
packet(enableBuffering: true) {
p: createProductParty(input: {
code: "123", name:"456"
}) {
id
code
name
}
}
}
Настройки на стороне сервиса DataSpace Core#
dataspace.buffering.workers-count
Значение по умолчанию: 0
Количество обработчиков, формирующих пачки
Значение больше 0 включает механизм буферизации на стороне DataSpace Core
dataspace.buffering.max-workers-count
Значение по умолчанию: 4
Максимальное количество обработчиков
Если dataspace.buffering.workers-count > dataspace.buffering.max-workers-count в лог-запись будет выведено предупреждение и будет использовано значение dataspace.buffering.max-workers-count
dataspace.buffering.enable-by-default
Значение по умолчанию: false
Буферизация запросов по умолчанию
Если в запросе не установлен параметр enableBuffering, то используется значение этой настройки для решения буферизировать запрос или нет
dataspace.buffering.worker-mode
Значение по умолчанию: event_loop
Алгоритм формирования пачек.
dataspace.buffering.commit-by-empty-buffer
Значение по умолчанию: false
Активация триггера завершения обработки пачки по событию отсутствия пакетов в очереди.
dataspace.buffering.max-packets-in-batch
Значение по умолчанию: 5
Пороговое значение для завершения обработки по событию достижения максимального размера пачки.
dataspace.buffering.time-window-ms
Значение по умолчанию: 20
Пороговое значение для завершения обработки по событию достижения максимального времени выполнения транзакции.
dataspace.buffering.poll-timeout-ms
Значение по умолчанию: 20
Максимальное время ожидание алгоритмом появления в очереди кандидатов на буферизацию.
dataspace.buffering.buffer-timeout-ms
Значение по умолчанию: 50000
Максимальное время ожидания основным потоком обработки запроса при помощи буферизации. Если это время будет превышено,
возникнет исключение sbp.sbt.sdk.exception.detailedexception.BufferTimeoutException, которое можно обработать на клиентской
стороне
Мониторинг#
Для мониторинга механизма буферизации предусмотрены метрики в формате Prometheus (см. раздел «Метрики буферизации запросов» документа «DataSpace Monitoring»).
Работа со справочниками DataSpace#
Загрузка справочников через компонент DataSpace Core#
Сервис DataSpace Core предоставляет потребителю возможность добавлять и изменять справочные данные во время работы приложения.
Для загрузки справочников предусмотрен отдельный endpoint DataSpace Core:
/api/dictionaries/upsert
Через данный endpoint можно добавить или обновить данные справочной сущности. Удалить справочную сущность невозможно.
Для загрузки справочных данных также, как и в случае наполнения справочными данными таблиц на этапе разворачивания сервиса, необходимо разметить сущности справочников (см. раздел «Разметка данных» документа «Руководство по ведению модели данных»).
Логика работы: по идентификатору находится существующая справочная запись и она обновляется. Если нет записи с данным идентификатором, создается новая запись.
Формат данных справочников совпадает с форматом, описанном в разделе «Работа с локальными справочниками» документа «Руководство по ведению модели данных» за исключением того, что для передачи на endpoint DataSpace Core необходимо поместить данные в массив. За один вызов можно передавать разные типы данных:
[
{
"type": "Region",
"objects": [
{
"id": "1",
"name": "Тульская губерния"
},
{
"id": "2",
"name": "Архангельская область"
}
]
},
{
"type": "City",
"objects": [
{
"id": "1",
"region": "1",
"name": "Новосиль"
},
{
"id": "2",
"region": "1",
"name": "Одоев"
},
{
"id": "3",
"region": "2",
"name": "Онега"
}
]
}
]
Обновление данных происходит в одной транзакции. Таким образом необходимо обеспечить разумный объем передаваемых данных в соответствии с сетевыми ограничениями и ограничениями БД.
Для работы с API можно воспользоваться любым HTTP-клиентом. Необходимо отправить POST-запрос с указанием необходимой кодировки и Content-Type: application/json.
Формат загрузки предоставляет возможность удаления записей справочников. Пример удаления в справочнике City записи с идентификатором 2:
[
{
"type": "City",
"deletes": ["2"]
}
]
Поле deleted представляет массив идентификаторов типа type которые будут удалены.
Внимание!
При использовании команд удаления необходимо учитывать отсутствие контроля ссылочной целостности, что может привести к появлению битых ссылок.
Поддержка режима DryRun#
API загрузки справочников /dictionary/upsert поддерживает HTTP-заголовок X-Dry-Run: True.
При наличии этого заголовка данные в БД реально не загружаются, но проверяется возможность их загрузки — анализируется структура и содержимое файла и возвращается ответ со статусом «202», содержащий в теле отчет об изменениях, которые будут выполнены в случае применения передаваемых в сервис данных.
Пример отчета:
{
"upsert": [
{ "type": "Currencies", "total": 15, "updated": "5", "created" : "10" },
{ "type": "CustomerType", "total": 3, "updated": "1", "created" : "2" },
{ "type": "OKATO", "total": 100, "updated": "100", "created" : "0" }
]
}
В отчете:
типы приводятся как в исходных данных (без учета наследования);
created отражает количество созданных записей;
updated отражает количество существующих записей.
В случае неуспеха возвращается HTTP-статус кода «202», и ответ:
{
"ERROR": <описание ошибки>
}
HTTP-статусы успешного и неуспешного ответа могут быть переопределены параметрами:
dataspace.dictionary.dryRun.successHttpStatus— для успешного ответа;dataspace.dictionary.dryRun.errorHttpStatus— для неуспешного ответа.
Если данные не соответствуют формату, то HTTP-статус кода ответа всегда равен «400» и переопределению не подлежит.
Изменение справочников через пакет команд#
Кроме /api/dictionaries/upsert выполнять изменения справочников можно через пакет команд.
Для справочников в SDK генерируется класс пакета DictionaryPacket с командами get, updateOrCreate и delete.
Внимание!
При использовании команд удаления необходимо учитывать отсутствие контроля ссылочной целостности, что может привести к появлению битых ссылок.
Так как все справочники относятся к одному агрегату, нет необходимости расширения границы транзакции пакета.
Для статусных справочников допускаются только команды чтения.
Генерацию класса DictionaryPacket можно отключить параметром <allowDictionaryPacket>false</allowDictionaryPacket> плагина model-api-generator-maven-plugin для цели createSdk.
Пример пакета команд для создания и чтения справочников:
@Test
void dictionaryTest() {
final DictionaryPacket dictionaryPacket = DictionaryPacket.createPacket();
final RegionRef region1Ref = dictionaryPacket.region.updateOrCreate(CreateRegionParam.create()
.setId("1").setName("Тульская губерния")
);
dictionaryPacket.city.updateOrCreate(CreateCityParam.create()
.setId("1").setRegion(region1Ref).setName("Новосиль")
);
final RegionRef region2Ref = dictionaryPacket.region.updateOrCreate(CreateRegionParam.create()
.setId("2").setName("Архангельская область")
);
dictionaryPacket.city.updateOrCreate(CreateCityParam.create()
.setId("2").setRegion(region2Ref).setName("Одоев")
);
dictionaryPacket.city.updateOrCreate(CreateCityParam.create()
.setId("3").setRegion(region2Ref).setName("Онега")
);
final RegionGet region1Get = dictionaryPacket.region.get(
region1Ref,
g -> g.withName().withCities(CityCollectionWith::withName)
);
final RegionGet region2Get = dictionaryPacket.region.get(
region2Ref,
g -> g.withName().withCities(CityCollectionWith::withName)
);
assertThatCode(() -> packetClient.execute(dictionaryPacket)).doesNotThrowAnyException();
assertThat(region1Get.getName()).isEqualTo("Тульская губерния");
assertThat(region1Get.getCities().stream().map(CityGet::getName).collect(Collectors.toSet()))
.containsExactly("Новосиль");
assertThat(region2Get.getName()).isEqualTo("Архангельская область");
assertThat(region2Get.getCities().stream().map(CityGet::getName).collect(Collectors.toSet()))
.containsExactlyInAnyOrder("Онега", "Одоев");
}
Загрузка через служебную таблицу#
В связи с появлением требований по формированию векторов изменений при корректировке справочных данных и отправке их в ТСА (Технологический сервис архивирования) и далее в КАП (Корпоративная аналитическая платформа) были произведены доработки в механизме загрузки справочных данных. Новый механизм также распространяется на заполнение данных по статусам.
В качестве обеспечения обратной совместимости по умолчанию будет работать прежняя логика загрузки. Для активации новой логики требуется добавить два параметра (см. раздел «Описание настроек»).
Описание логики загрузки#
Если ранее справочные данные грузились непосредственно в соответствующие таблицы БД, то теперь ими наполняются служебные таблицы. Заполнение таблиц БД со справочными данными происходит в момент поднятия модуля dataspace-core, который просматривает служебные таблицы, вычитывает из них необходимую информацию и заполняет таблицы данных, попутно формируя вектора изменений. Загружаются данные для версии модели равной и меньшей текущей. Сравнение версии модели происходит по стандарту semVer.
Описание настроек#
Для включения механизма необходимо выполнить две настройки:
Для цели
createModelmaven-плагинаmodel-api-generator-maven-pluginуказать параметр<loadDictionaryByTable>true</loadDictionaryByTable>. Этот параметр при формировании Liquibase-скриптов перенаправит загрузку справочных данных в служебные таблицы.В список параметров запуска модуля dataspace-core необходимо добавить
dataspace.dictionary.tableLoad.enable=true.
Возможные ошибки и их причины#
Если при поднятии dataspace-core получаем ошибку SysVersionNotFoundException с текстом Версия схемы данных не соответствует версии модели данных модуля(<версия модели>)., то это означает, что происходит попытка поднять модуль с версией модели более новой, чем была прогружена на схему. Сравнивается версия в pdm.xml с наличием объекта класса SysVersion с той же версией. Если объект не найден — получаем ошибку. Обычная причина ошибки — на схеме не прогружены последние изменения.
Стратегия генерации ID с заданным строковым префиксом#
Для реализации механизма вытеснения данных из оперативной базы имеется возможность генерировать идентификаторы сущностей одного агрегата с добавлением произвольного строкового префикса.
В описании класса корня агрегата необходимо указать атрибут id-prefixed="true".
После генерации модели в параметрах команды создания экземпляра корня агрегата станет доступным к заполнению атрибут sysIdPrefix.
При использовании необходимо учитывать следующее:
действие настройки распространяется только на новые экземпляры;
атрибут
sysIdPrefixустанавливается только один раз при создании экземпляра корня агрегата;sysIdPrefixможно указать только для корня агрегата. Все дочерние объекты при генерации ID будут содержать данный префикс;для дочернего элемента необходимо явно задавать ID или при генерации будет добавлен соответствующий
sysIdPrefixкорня;отключение параметра не предусмотрено;
при создании сущности с указанием идентификатора (
MANUAL,AUTO_ON_EMPTYилиUUIDV4_ON_EMPTY) значениеsysIdPrefixдолжно быть указано в таком идентификаторе.
Модель технического агрегата для демонстрации использования sysIdPrefix:
<model>
<class name="PrefixedIdAggregate" id-prefixed="true">
<id category="AUTO_ON_EMPTY"/>
<property name="elements" type="PrefixedIdAggregateElement" collection="set" mappedBy="owner"/>
</class>
<class name="PrefixedIdAggregateElement">
<id category="AUTO_ON_EMPTY"/>
<property name="owner" type="PrefixedIdAggregate" parent="true"/>
</class>
</model>
Оба класса имеют категорию генерации идентификатора AUTO_ON_EMPTY для демонстрации использования пользовательского значения.
Пример успешного использования префикса:
// используемый префикс
final String idPrefix = "SAMPLE_";
// создание корня агрегата и одного элемента агрегата с генерируемыми идентификаторами
final Packet createAggregatePacket = Packet.createPacket();
final PrefixedIdAggregateRef aggregateRef = createAggregatePacket
.prefixedIdAggregate
.create(p -> p.setSysIdPrefix(idPrefix));
final PrefixedIdAggregateElementRef aggregateElementRef = createAggregatePacket
.prefixedIdAggregateElement
.create(p -> p.setOwner(aggregateRef));
assertThatCode(() -> dataspaceCorePacketClient().execute(createAggregatePacket)).doesNotThrowAnyException();
assertThat(aggregateRef.getId()).startsWith(idPrefix);
assertThat(aggregateElementRef.getId()).startsWith(idPrefix);
// создание элемента агрегата с пользовательским идентификатором
final String userId = idPrefix + UUID.randomUUID();
final Packet createAggregateElementPacket = Packet.createPacket();
final PrefixedIdAggregateElementRef nextElementRef = createAggregateElementPacket
.prefixedIdAggregateElement
.create(p -> p
.setId(userId)
.setOwner(aggregateRef)
);
assertThatCode(() -> dataspaceCorePacketClient().execute(createAggregateElementPacket)).doesNotThrowAnyException();
assertThat(nextElementRef.getId()).isEqualTo(userId);
Пример ошибки при отсутствии заполненного sysIdPrefix:
final Packet packet = Packet.createPacket();
packet.prefixedIdAggregate.create(CreatePrefixedIdAggregateParam.create());
assertThatCode(() -> dataspaceCorePacketClient().execute(packet))
.isInstanceOf(InvalidArgumentException.class)
.hasMessageContaining("Отсутствуют обязательные к заполнению поля [sysIdPrefix]");
Пример ошибки при отсутствии префикса в указываемом идентификаторе сущности:
final Packet packet = Packet.createPacket();
packet.prefixedIdAggregate.create(
CreatePrefixedIdAggregateParam
.create()
.setId("42")
.setSysIdPrefix("SAMPLE_")
);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet))
.isInstanceOf(DataAccessException.class)
.hasMessageContaining("Идентификатор '42' должен содержать префикс 'SAMPLE_'");
Пример ошибки при отсутствии префикса в указываемом идентификаторе сущности для элемента агрегата:
final Packet packet = Packet.createPacket();
final PrefixedIdAggregateRef aggregateRef = packet.prefixedIdAggregate.create(
CreatePrefixedIdAggregateParam
.create()
.setSysIdPrefix("SAMPLE_")
);
packet.prefixedIdAggregateElement.create(p -> p
.setId("42")
.setOwner(aggregateRef)
);
assertThatCode(() -> dataspaceCorePacketClient().execute(packet))
.isInstanceOf(DataAccessException.class)
.hasMessageContaining("Идентификатор '42' должен содержать префикс 'SAMPLE_'");
Механизм выполнения запросов с историей и поддержкой контроля второй рукой (КВР) Request Control#
Request Control — механизм контроля выполнения запросов, обеспечивающий возможность их предварительной валидации, хранения, согласования и последующего выполнения с фиксацией этапов процесса, включая исполнителей, причину запроса, время выполнения, возможные ошибки и другие данные.
Примечание
Request Control так же можно применить для разграничения доступа - см. раздел разграничения доступа к данным.
Механизм поддерживается в двух сценариях взаимодействия:
Через GraphQL API
Через REST API разграничения доступа к данным.
Включение механизма производится с помощью специального HTTP-заголовка X-DSPC-Requestcontrol, содержащего Base64-кодированную JSON-структуру с параметрами контроля выполнения запроса.
Формат структуры:
{
"action": "<Action>",
"login": "<UserLogin>",
"userName": "<FullName>",
"ref": "<IncidentLink>",
"reason": "<Description>",
"opId": "<OperationID>"
}
Параметры структуры:#
action: Тип действия (Action), определяет дальнейшую обработку запроса (например, проверка и сохранение, согласие, отказ и др., описаны далее).login: Учетная запись пользователя, от чьего лица производится текущее действие.userName: Полное имя пользователя.ref: Идентификатор инцидента (только для действийValidateAndStoreиValidateAndExecute, описанных далее).reason: Причина создания/отклонения запроса (только для действийValidateAndStore,ValidateAndExecuteиRejectStored, описанных далее).opId: Уникальный идентификатор операции. Задается вызывающей стороной, на всех этапах механизма
Возможные действия (Action)#
При использовании механизма RequestControl, запрос на каждом шаге должен содержать идентичные тело и параметры запроса.
Значение |
Описание |
|---|---|
|
Проверка и сохранение запроса для последующей обработки |
|
Немедленное выполнение после успешной проверки |
|
Подтверждение ранее провалидированного и сохраненного запроса |
|
Одновременное подтверждение и выполнение сохраненного запроса |
|
Отклонение сохраненного запроса |
|
Запуск выполнения подтвержденного запроса |
Состояния запроса#
Запрос может находиться в одном из пяти состояний:
Код состояния |
Описание |
|---|---|
|
Запрос сохранен, прошел проверку |
|
Запрос отклонен |
|
Запрос одобрен |
|
Запрос успешно выполнен |
|
Запрос завершился с ошибкой |
Логика работы#
К запросам в зависимости от их состояния могут быть применены действия, указанные в таблице:
Текущее состояние |
Допустимое действие |
Новое состояние |
|---|---|---|
Отсутствует |
ValidateAndStore |
VALID |
Отсутствует |
ValidateAndStore |
REJECTED |
Отсутствует |
ValidateAndExecute |
REJECTED |
Отсутствует |
ValidateAndExecute |
SUCCEEDED |
Отсутствует |
ValidateAndExecute |
FAILED |
VALID |
ApproveStored |
APPROVED |
VALID |
RejectStored |
REJECTED |
VALID |
ExecuteStored |
SUCCEEDED |
VALID |
ExecuteStored |
FAILED |
APPROVED |
RejectStored |
REJECTED |
APPROVED |
ExecuteApproved |
SUCCEEDED |
APPROVED |
ExecuteApproved |
FAILED |
При этом создаваемые запросы:
проходят проверку (Dry-Run) перед сохранением/выполнением, в ходе которой выявляются потенциальные проблемы;
и выполняются дополнительные проверки:
инициатор запроса не может подтвердить или выполнить с подтверждением свой сохраненный запрос;
тело и параметры GraphQL-запроса идентичны переданным при создании GraphQL-запроса (для предотвращения изменений в процессе выполнения контроля).
Формат ответа#
Формат для GraphQL протокола#
{
"data": ..., // Данные результата, если было выполнение
"errors": ..., // Ошибки выполнения, если имелись
"extensions": {
"requestControl": {
"status": "VALID", // Текущий статус запроса, в результате действия
"errors": ..., // Ошибки логики Request Control (например невалидный переход)
"dry-run": ... // Результат dry-run (для действий, содержащих валидацию - `ValidateAndStore`, `ValidateAndExecute`)
}
}
}
Формат для REST API разграничения доступа к данным#
При использовании механизма Request Control формат ответа на административном REST API остается неизменным (за исключением наличия результата, т.к. при определенных действиях выполнения не происходит).
История запросов Request Control#
При использовании Request Control история запроса сохраняется в сущности SysRequestControl, доступной для чтения через GraphQL API. Основные поля включают:
operationName: Название операции (из шаблона (описаны далее), в случае GraphQL или имя endpoint’а, в случае Rest).source: Источник запроса (graphqlили/security/permissions).query: Тело запроса.params: Параметры.reason: Причина создания запроса.ref: Связанный инцидент.initiatorLogin: Пользователь, инициировавший запрос.approverLogin: Пользователь, согласовавший запрос.creationTime: Время создания запроса.decisionTime: Время утверждения или отклонения.executionTime: Время завершения запроса.status: Текущий статус запроса.rejectReason: Причина отклонения (в случае отклонения).errors: Информация об ошибках выполнения.
Шаблоны запросов Request Control#
Шаблоны влияют исключительно на запросы, проходящие через Request Control,
обеспечивая дополнительную проверку соответствия шаблону (идентификация осуществляется по operationName) при создании нового запроса на его основе.
Создание шаблонов возможно c помощью Rest API разграничения доступа к данным с дополнительным query-параметром target:
API— правила разграничения доступа к GraphQL-запросам (значение по умолчанию), см. разграничения доступа к данным.ADM— шаблоны Request Control (основное предназначение - использование для ведения шаблонов запросов для компонента DSAC продукта Platform V Dataspace).
Пример использования#
Пример сквозного сценария от создания до выполнения запроса с использованием Request Control на протоколе GraphQL:
Создание запроса:
mutation EnableUserAccount { packet { updateUser(input: {id: "12345", enabled: true}) } } # Заголовок 'X-DSPC-Requestcontrol' (до кодирования в base64) { "action": "ValidateAndStore", "login": "administrator@example.com", "userName": "Иван Иванов", "ref": "issues#54321", "reason": "Восстановления профиля пользователя", "opId": "OP-Enable-User-12345" }Согласование (контроль второй рукой):
mutation EnableUserAccount { packet { updateUser(input: {id: "12345", enabled: true}) } } # Заголовок 'X-DSPC-Requestcontrol' (до кодирования в base64) { "action": "ApproveStored", "login": "second_controlling_administrator@example.com", "userName": "Петр Петрович", "opId": "OP-Enable-User-12345" }Выполнение:
mutation EnableUserAccount {
packet {
updateUser(input: {id: "12345", enabled: true})
}
}
# Заголовок 'X-DSPC-Requestcontrol' (до кодирования в base64)
{
"action": "ExecuteApproved",
"login": "administrator@example.com",
"userName": "Иван Иванов",
"opId": "OP-Enable-User-12345"
}
Результат#
После изучения данного документа прикладной разработчик сможет использовать SDK, предоставляемый DataSpace Core (DSPC), для реализации доступа к данным при разработке функционала своего приложения.