ym88659208ym87991671
Использование DataSpace SDK | Документация для разработчиков
Skip to main content

Использование DataSpace SDK

Обновлено 20 апреля 2022

Использование SDK для формирования пакета команд

Взаимодействие с серверной частью DataSpace осуществляется по протоколу JSON-RPC 2.0 через точку доступа со следующим URL-адресом: {серверURL}/packet (можно посмотреть на вкладке Детали вашего проекта).

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

  • DataspaceCorePacketClient: клиент для взаимодействия с контроллером сервиса по обслуживанию пакета изменений;
  • Packet: генерируемый на основе пользовательской модели адаптер протокола для работы с пакетом изменений.

В примерах используется модель.

Клиент DataspaceCorePacketClient

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

DataspaceCorePacketClient packetClient = new DataspaceCorePacketClient("http://127.0.0.1:8080");
note

Вместо http://127.0.0.1:8080 указывается адрес без указания конкретного endpoint.

Подробно о вариантах клиента и возможностях конфигурации описано в разделе DataSpace-клиенты в DataSpace SDK.

Вызов сервиса для выполнения пакета производится методом execute:

packetClient.execute(Packet.createPacket());

Параметр метода принимает пакет изменений, который должен исполнить сервис. Класс Packet рассмотрен далее в разделе Пакет изменений Packet. Если выполнение метода не вызвало исключения, то пакет обработан успешно. В противном случае будет задействован SdkJsonRpcClientException или наследованное от этого класса исключение:

  • ObjectNotFoundException: сущность с указанным идентификатором не найдена;
  • ParseException: ошибка разбора запроса;
  • InvalidArgumentException: ошибка аргумента в запросе;
  • DataAccessException: ошибка уровня базы данных;
  • DataAccessConstraintException: ошибка уровня базы данных о нарушении ограничения;
  • IdempotencyException: ошибка идемпотентного вызова;
  • StatusException: ошибка статуса;
  • AggregateException: ошибка использования агрегата;
  • AggregateVersionException: ошибка версии агрегата;
  • SystemLockException: ошибка системной блокировки;
  • ApplicationLockException: ошибка прикладной блокировки;
  • MaskNotMatchException: ошибка проверки значения маске;
  • CompareNotEqualException: ошибка сравнения значений;
  • ConnectTimeoutException: таймаут соединения;
  • ReadTimeoutException: таймаут чтения.

Возможна отправка нескольких пакетов (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.

Пакет изменений 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();

Пояснение к примеру:

  1. Создается экземпляр пакета для последующего заполнения командами.
  2. packet содержит свойство productParty, предоставляющее доступ к командам для этого типа модели.
  3. Метод create отражает команду пакета create, которая предназначена для создания экземпляра. В примере не определены значения для создаваемой сущности.
  4. Команда create возвращает идентификатор созданной сущности, но это событие произойдет при выполнении пакета сервером. Результат метода create типа ProductPartyRef является указателем на результат выполнения команды.
  5. Пакет передается на исполнение сервисом через клиента dataspaceCorePacketClient().execute(packet).
  6. После успешного выполнения пакета указатель команды productPartyRef содержит значение идентификатора созданной сущности.

Следует отметить, что пакет может быть исполнен успешно только один раз. Свойство packet.isExecuted() примет значение true для выполненного пакета. Другие свойства пакета, заполняемые после выполнения:

  • isIdempotenceResponse(): принимает значение true для повторного вызова идемпотентного пакета (подробнее см. в разделе Идемпотентность);
  • 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("наименование продукта")
);

Правила проекции свойств типа модели на атрибуты команды

В параметры команды входят все свойства типа сущности, описанные в модели данных. Имена свойств совпадают с атрибутами команды. При выполнении команды значения ссылочных свойств (кроме внешних ссылок) проверяются на существование сущности. Если сущность не найдена, команда генерирует исключение ObjectNotFoundException.

Идентификатор сущности setId()

Особенности:

  • обязательный с типом сущности, определяющим категорию идентификатора MANUAL;
  • опциональный с типом сущности, определяющим категорию идентификатора AUTO_ON_EMPTY;
  • отсутствует с типом сущности, определяющим категорию идентификатора AUTO (SNOWFLAKE).

Внешние ссылки и коллекции внешних ссылок

Для внешних ссылок генерируются классы, обеспечивающие строгую типизацию в параметрах команды. Имя класса составляется по правилу ИмяТипаВнешнейСсылки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): удаление всех элементов в коллекции с последующим добавлением.
note

Возможность удаления элементов коллекции актуальна при изменении сущности.

Пример заполнения коллекции внешних ссылок:

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")
)
)
);

Статус

Если для типа сущности определена статусная модель, то в параметрах команды становятся доступны методы с шаблонными именами statusForКодНаблюдателя. Параметры метода:

  • обязательный параметр устанавливаемого сущности статуса;
  • опциональный параметр описания причины перехода.

Пример заполнения статусов для двух наблюдателей:

final Packet packet = Packet.createPacket();

final ProductPartyRef ref = packet.productParty.create(p -> p
.setStatusForPlatform(ProductPartyPlatformStatus.PRODUCTCREATED)
.setStatusForService(
ProductPartyServiceStatus.DEPOSITOPENED,
"демонстрация статуса")
);

Команда создания или изменения сущности updateOrCreate

Команда updateOrCreate выполняет поиск сущности по ключевым критериям. Если сущность найдена, выполняется ее обновление, иначе — создается новый экземпляр.

Доступность команды для типа зависит от наличия одного из условий:

  • категория идентификатора MANUAL;
  • категория идентификатора AUTO_ON_EMPTY;
  • для категории идентификатора AUTO (SNOWFLAKE) обязательно наличие уникального индекса.
note

Для стратегии наследования SINGLE_TABLE учитывается доступность индексов предков.

Сигнатура метода на примере 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 свойство является опциональным. Логика команды требует определенного критерия, который однозначно идентифицирует сущность. Если заполнен id, то поиск выполняется по значению этого свойства. Если id не используется, то обязательно должен быть заполнен параметр метода имени уникального ключа. Значения для поиска определяются следующим образом:

  1. Используется значение для свойства из состава индекса, указанное в параметрах команды.
  2. Если атрибут не указан, то используется значение по умолчанию для поля.
  3. В противном случае – значение «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. В примере параметры не имеют явного указания значения этого свойства, но второй вызов метода не создаст новую сущность, так как существует запись со значением по умолчанию, которое будет использовано при формировании критерия поиска.

Команда изменения сущности 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 можно определить проверку на соответствие фактических значений сущности ожидаемым. Если по одному из указанных свойств значение не совпадает, то команда завершится ошибкой. Проверка выполняется до внесения изменений по свойствам.

  • Использование 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.

Команда чтения сущности get

Команда позволяет получить состояние сущности в момент применения. Показанное в примере применение команды get похоже на «грязное чтение». Пакет выполняется в одной транзакции и при успешном завершении зафиксируются конечные свойства сущностей. Все команды пакета выполняются последовательно, поэтому чтение возвращает состояние в моменте своего выполнения. Сигнатура команды на примере ProductParty:

ProductPartyGet get(ProductPartyRef productParty, Consumer<ProductPartyWith> param)

Генерируемые классы ProductPartyWith и ProductPartyGet описывают запрашиваемые свойства и результат после выполнения команды на сервере соответственно.

Команда построена на базе метода search с ограничением поиска одной сущности по идентификатору. Для отсутствующей сущности команда генерирует исключение ObjectNotFoundException. Подробнее о заполнении параметра With и составе результата Get описано в разделе Использование поискового SDK.

Пример использования команды:

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

Команда удаления сущности 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' значений");

Команды прикладной блокировки сущности tryLock и unlock

Команды доступны для типов модели с указанием атрибута класса lockable="true". Сигнатуры методов на примере ProductParty:

LockRs tryLock(ProductPartyRef productParty, Consumer<LockRq> param);
LockRs unlock(ProductPartyRef productParty, String appLockToken);

Подробно о работе команды, структуре классов LockRs и LockRq описано в разделе Использование прикладных блокировок ресурсов.

Использование поискового SDK

danger

Введено ограничение на общее количество выбираемых данных размером в 10 000. В это ограничение входят как выборки корневых элементов, так и любых вложенных элементов. Если выдача по запросу превышает заданное ограничение, то генерируется исключени ReadRecordsCountExceededLimitException.

Поисковый сервис DataSpace (на стороне сервера) позволяет выполнять динамические поисковые запросы. Под динамичностью понимается возможность формирования произвольных поисковых запросов потребителем, а не использование предопределенных запросов.

Для упрощения взаимодействия с поисковым сервисом предусмотрен SDK, который можно использовать на стороне клиента.

Взаимодействие с поисковым сервисом может осуществляться и без использования SDK.

Поисковый сервис

Взаимодействие с серверной частью DataSpace осуществляется по протоколам JSON-RPC 2.0 через точку доступа со следующим URL-адресом: {серверURL}/search (можно посмотреть на вкладке Детали вашего проекта).

Поисковое SDK (GraphDTO)

Поисковое SDK основано на модели потребителя и призвано упросить взаимодействие с серверной частью.

Для поиска предоставляется инструмент GraphDTO. Основная идея состоит в чтении только необходимых полей сущности, а не всех доступных.

Поисковые классы имеют названия, совпадающие с названиями сущностей модели потребителя с добавлением специфичных постфиксов:

  • <имя сущности>Graph — используется при создании поискового запроса и разборе ответа (служебные классы);
  • <имя сущности>Grasp — используется при формировании поисковых условий (раздел where в терминах sql);
  • <имя сущности>Get — интерфейс для работы с результатом поиска;
  • <имя сущности>With и <имя сущности>CollectionWith — интерфейсы для описания поискового запроса;
  • <имя сущности>WithPicker, <имя сущности>CollectionWithPicker и <имя сущности>GetPicker — упрощают передачу уточняющих классов в методы (классы-расширений), исключают возможность передачи некорректного класса в методы.

Классы:

  • GraspHelper — класс с вспомогательными функциями, применяемыми при построении поисковых условий (например, upper(), lower(), round(), ceil() и др.).
  • DataspaceCoreSearchClient — класс, используемый для взаимодействия с серверной частью в рамках поисков (отправки поискового запроса на сервер и получения результата).
  • GraphCreator — класс, содержащий методы инициализации поисковых запросов для всех сущностей модели.

Создание запроса

Создать запрос возможно одним из следующих способов:

  • с помощью класса raphCreator;
  • с помощью соответствующего искомой сущности 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().

note

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

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

Запрос примитивных полей

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

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());

Запрос идентификаторов вложенных объектов и коллекций объектов

Если необходимо получить только идентификатор вложенного объекта или коллекции вложенных объектов, то необходимо вызвать соответствующий метод with без параметров. Для коллекции объектов можно передать лямбду с настройкой ограничивающих условий и фильтрации (рассматривается далее).

Иными словами, если спецификация сущности не передана или не содержит возвращаемых данных, то по умолчанию всегда возвращается идентификатор сущности.

Пример создания запроса с вложенным объектом:

GraphCreator.selectPerformedService()
.withCode()
.withName()
// запрос идентификатора вложенной сущности
.withProduct()
// запрос идентификаторов сущностей из коллекции
.withPerfomedOperations();

Уточнение типа вложенного объекта и коллекции объектов

Ссылочные поля (и коллекции ссылок) могут «ссылаться» не на базовый класс, а на класс-потомок. Например, поле Product может ссылаться не на ProductParty, а на его потомка — DepositCBExmpl.

В такой ситуации имеется возможность уточнить тип ссылочного поля (или коллекции ссылок). Для запроса ссылочного поля или коллекции ссылок с уточнением типа используются перегруженные методы с такими же названиями, что и при запросе без уточнения типа. При этом уточняющий тип передается первым параметром метода, а лямбда — вторым. При этом в лямбде появляются методы для запроса полей указанного потомка.

Пример создания запроса на класс расширения:

GraphCreator.selectPerformedService()
.withCode()
.withName()
.withProduct(ProductPartyCollectionWIthPicker::DepositCBExmpl, ppWith->
ppWith.withDeclaration()
.withLastCptDate())
.withPerformedOperations(PerfromedOperationCollectionWithPicker::BigOperation, poWith ->
poWith.withCode()
.withBigCost());

Детализация коллекции объектов по потомкам

Если в коллекции (корневой или вложенной) лежат объекты разных типов (базовый тип и его потомки), то имеется возможность одним запросом выбрать объекты произвольных типов. Для этого необходимо запросить для коллекции минимально необходимый базовый тип (объединяющий всех потомков) и затем детализировать выборку по его потомкам.

В примере ниже запрашиваются объекты типа DepositCBReplenish. При этом, если объект является не просто DepositCBReplenish, а потомком типа DepositCBReplenishPlus, то необходимо дополнительно выбрать указанные атрибуты этого потомка.

Детализация корневой коллекции объектов по потомкам

Пример запроса:

GraphCreator.selectDepositCBReplenish()
.withTerBankCode()
.withTerBankName()
// Детализируем запрашиваемые данные по потомкам.
// Первым параметром указываем тип потомка DepositCBReplenish,
// вторым — описываем характерные для потомка данные.
.extend(DepositCBReplenishWithPicker::DepositCBReplenishPlus, p -> p
.withTerBankNameOnline()
.withTerBankNamePlus()
);

Детализация вложенной коллекции объектов по потомкам

В примере ниже для корневого объекта запрашивается вложенная коллекция элементов типа ProductRegisterDepositCBReplenish. При этом для дочерних элементов типов ProductRegisterDepositCBReplenishMainWith и ProductRegisterDepositCBReplenishAdditionalWith запрашивается разный набор данных. Для выполнения этого запроса ProductRegisterDepositCBReplenishMainWith и ProductRegisterDepositCBReplenishAdditionalWith необязательно должны находиться в одной цепочки иерархии, но должны иметь общего предка — ProductRegisterDepositCBReplenish.

Пример запроса:

GraphCreator.selectDepositCBReplenish()
.withTerBankCode()
.withTerBankName()
// Запрашиваем вложенную коллекцию, уточняем базовый запрашиваемого тип объекта
.withProductRegisters(ProductRegisterDepositCBReplenishPicker::ProductRegisterDepositCBReplenish, p -> p
.withRegisterNumber()
.withFirstValue()
// Детализируем тип объекта вложенной коллекции по потомкам
// Оба типа расширяют тип ProductRegisterDepositCBReplenish
.extend(ProductRegisterDepositCBReplenishMainWith.class,
g -> g.withCodeMain())
.extend(ProductRegisterDepositCBReplenishAdditionalWith.class,
g -> g.withCodeAdd())
);

Установка ограничений на вложенную коллекцию

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

Пример запроса с ограничением вложенных выбираемых данных:

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, testCodeProduct2));

При фильтрации или сортировке данных для разыменования необходимо использовать метод entity() на ссылке. Этот метод предоставляет доступ к атрибутам соответствующей сущности.

    GraphCreator.selectPerformedOperation()
.withCode()
// В условии фильтрации на объекте с внешней ссылкой на ссылке появляется виртуальный метод с именем entity,
// по которому можно построить условие
.setWhere(where -> where.product().entity().codeEq(testCodeProduct2));
note

Типизация внешних ссылок и сортировка по коллекции внешних ссылок (с использованием агрегатных функций) находится в разработке.

Использование расчетных полей

Расчетные поля — это поля, которые отсутствуют на объекте и позволяют запросить некоторую дополнительную информацию.

Расчетные поля используются, например, для получения части коллекции, удовлетворяющей определенным условиям (например, получение действующих и завершенных сервисов в разных коллекциях).

Расчетные поля могут быть следующих видов:

  • Примитивное расчетное поле — поле, значением которого является «примитивный» тип (String, Long, Integer, Date, OffsetDateTime и т.п.). Такое расчетное поле может использоваться, например, для получения размера вложенной коллекции, определения максимального или минимального значения поля в коллекции и др.
  • Расчетное поле на базе внутренней ссылки — поле, значением которого является объект того же типа, что и поле. Пример использования: получение вложенного объекта под другим именем поля (алиас).
  • Расчетное поле на базе коллекции внутренних ссылок — поле, значением которого является коллекция, тип которой совпадает с типом поля.
note

В формировании расчетных полей не может участвовать тип 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-запросах осуществляется таким же образом, как и сортировка по расчетным полям.

Использование 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 осуществляется таким же образом, как и сортировка по расчетным полям.

Ограничение выборки и сортировка

Для ограничения выборки и сортировки используются методы интерфейса AbstractProxyCollectionWith, начинающиеся с префикса set.

К таким методам относятся:

  • setWhere − определяет условие фильтрации данных.
  • setLimit − ограничение на количество возвращаемых элементов.
  • setOffset − смещение запрашиваемых данных.
  • setSortingAdvanced − задает любые условия сортировки, включая атрибуты корневой, вложенных сущностей, результатов агрегирующих функций.
  • setTotalCount − маркер, информирующий о необходимости подсчета общего количества элементов, удовлетворяющих условию фильтрации данных (без учета limit и offset).
  • setNeedAggregateVersion − устанавливает признак необходимости получения версии агрегата для корневых объектов выборки. Получение результата осуществляется через метод $getAggregateVersion().

Метод setWhere (ранее метод setGrasp) позволяет задать условие фильтрации сущностей. Условие фильтрации задается при помощи лямбды (в которую передается Grasp-объект). Пример поиска с ограничивающими условиями:

GraphCreator.selectProductParty()
.withName()
.withCode()
.withPerformedServices(PerformedServiceCollectionWIthPicker::DepositOpenSrvCBExmpl, psWith->
psWith.withChannel()
.withBeginDate()
// ограничение вложенной коллекции
.setWhere(psWhere -> psWhere.codeLike("someCode%"))
.setLimit(25))
// ограничение корневой искомой сущности
.setWhere(ppWhere -> ppWhere.nameLike("someName%"))
.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("%")))
)
)
note

Через метод $link() можно обращаться только к вышестоящим сущностям. Обратиться к атрибутам вложенных сущностей можно через ссылки на объекте.

Пример использования метода $link() приведен в приложенном файле из проекта dataspace-client (класс SearchDemoTests, метод searchWithHierarchyTest).

danger

В подобных конструкциях нельзя использовать замыкания вышестоящих 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 ->
// onwer (Client) — SoftReference, в котором есть только один идентификатор — entityId
where.ownerEq(clientId)
// initialService — ComplexReference, в котором есть как entityId, так и rootEntityId
.and(where.performedOperationsContains(CreditOperationGrasp.class, po ->
po.initialServiceEq("serviceId", "aggreateId"))));

// Альтернативный способ (менее лаконичный, но предоставляет большие возможности)
GraphCreator.selectProduct()
.setWhere(where ->
// onwer (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%")));

Применение агрегирующих функций в условиях фильтрации

В 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));

Операции с числовыми полями

В 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())));

Запрос версии агрегата (setNeedAggregateVersion)

Пример запроса версии агрегата показан в следующем фрагменте кода:

GraphCreator.selectProductParty()
.withCode()
.setWhere(ppWhere -> ppWhere.codeLike("codePrefix%"))
// установка признака необходимости получения версии агрегата для корневого объекта
// на вложенных объектах данная функция не поддерживается и выбросит исключение в runtime
.setNeedAggregateVersion(true);
// Получение результата на объекте ответа
ProductPartyGet result = ... //Получение ответа — рассматривается ниже
Long aggregateVersion = result.$getAggregateVersion();

Сортировка объектов

Параметры сортировки устанавливаются при помощи метода setSortingAdvanced(). Этот метод обеспечивает возможность задавать сложные условия сортировки и защищенный вызов методов.

note

Также существует устаревший метод setSorting(). Метод 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 вспомогательные классы и методы показаны в таблице:

КлассОписание функцииПример кода
ConditionWrapperИнвертирует поисковой предикат (добавляет not перед предикатом)public static ConditionWrapper not(ConditionWrapper condition)
AndContainerСоздает пустой контейнер, объединяющий добавляемые в него предикаты через «and». Используется при динамическом формировании условий фильтрацииpublic static AndContainer emptyAnd()
OrContainerСоздает пустой контейнер, объединяющий добавляемые в него предикаты через OR. Используется при динамическом формировании условий фильтрацииpublic static OrContainer emptyOr()
NotContainerСоздает пустой контейнер, инвертирующий добавляемый в него предикат. Используется при динамическом формировании условий фильтрацииpublic static NotContainer emptyNot()
NumericFieldПереводит число в объект класса NumericFieldpublic static NumericField valueOf(Number value)
NumericFieldПереводит строку в объект класса NumericFieldpublic static NumericField stringAsNumber(String string);
NumericFieldПереводит строку или атрибут объекта, представленные классом StringField, в объект класса NumericFieldpublic static NumericField stringAsNumericField(StringField stringField);
BaseNumericFieldВозвращает округленное число или атрибут объекта, представленые классом BaseNumericFieldpublic static NumericField round(BaseNumericField baseNumericField)
BaseNumericFieldВозвращает округленное в большую сторону число или атрибут объекта, представленые классом BaseNumericFieldpublic static NumericField ceil(BaseNumericField baseNumericField)
BaseNumericFieldВозвращает округленное в меньшую сторону число или атрибут объекта, представленые классом BaseNumericFieldpublic static NumericField floor(BaseNumericField baseNumericField)
StringFieldПреобразует строку к классу StringField, который позволяет производить действия с полями объектов GraphDTOpublic static StringField valueOf(String value)
StringFieldПереводит строку или атрибут объекта, представленные классом StringField, в верхний регистрpublic static StringField upper(StringField stringField)
StringFieldПереводит строку или атрибут объекта, представленные классом StringField, в нижний регистрpublic static StringField upper(StringField stringField)
StringFieldПереводит строку или атрибут объекта, представленные классом StringField, в нижний регистрpublic static StringField trim(StringField stringField)
StringFieldУдаляет пробельные символы слева у строки или атрибута объекта, представленного классом StringFieldpublic static StringField ltrim(StringField stringField)
StringFieldУдаляет пробельные символы справа у строки или атрибута объекта, представленного классом StringFieldpublic static StringField rtrim(StringField stringField)
StringFieldВозвращает подстроку строки или атрибута объекта, представленного классом StringFieldpublic static StringField substr(StringField stringField, Integer start, Integer length)
StringFieldВозвращает подстроку строки или атрибута объекта, представленного классом StringFieldpublic static StringField substr(StringField stringField, NumericField start, NumericField length)
StringFieldВозвращает подстроку строки или атрибута объекта, представленного классом StringFieldpublic static StringField substr(StringField stringField, Integer start)
StringFieldВозвращает подстроку строки или атрибута объекта, представленного классом StringFieldpublic static StringField substr(StringField stringField, NumericField start)
StringFieldОсуществляет замену подстроки в строке или атрибуте объекта, представленного классом StringFieldpublic static StringField replace(StringField stringField, String oldValue, String newValue)
StringFieldОсуществляет замену подстроки в строке или атрибуте объекта, представленного классом StringFieldpublic static StringField replace(StringField stringField, StringField oldValue, StringField newValue)
StringFieldВозвращает длину строки или длину строкового атрибута объекта, представленного классом StringFieldpublic static NumericField length(StringField stringField)
StringFieldОбъединяет переданные строки или атрибуты объекта, представленные классом StringField, через заданный разделительpublic static StringField concat(String delimiter, StringField firstField, StringField... fields)
NumericField<?>Возвращает первое ненулевое (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))))
// Расширяем спецификацию для того, чтобы если класс найденного объекта будет Deposit,
// то получить у него поле minSum
.extend(ProductWithPicker::Deposit, DepositWith::withMinBalance);

Передача запроса на сервер и получение ответа

Запросы на сервер передаются при помощи экземпляра класса DataspaceCoreSearchClient, как показано в примере ниже. Конструктор класса принимает единственный параметр — URL серверной части.

DataspaceCoreSearchClient searchClient = new DataspaceCoreSearchClient("http://192.168.0.1:8080"); 
note

Вместо http://127.0.0.1:8080 указывается адрес без указания конкретного endpoint.

Рекомендуется использовать один экземпляр 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);

Объединение запросов к разным агрегатам

Функциональность объединения запросов к разным агрегатам (merge), обладающим одним интерфейсом, рассмотрим на примере. Предположим, необходимо вести события. При этом события могут быть двух видов:

  1. общие для всех организаций;
  2. относящиеся только к одной конкретной организации.

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

Необходимо вывести для заданной организации все события (и собственные, и общие) одним списком, упорядоченным по дате начала события с пагинацией.

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

Общие поля сущностей выделяют в интерфейс, который наследуют сущности. Подходящий под пример файл model.xml может иметь следующий вид:

<interface name="Event">
<property name="code" type="String"></property>
<property name="name" type="String"></property>
<property name="beginDate" type="Date"></property>
<property name="endDate" type="Date"></property>
</interface>
<class implements="Event" label="Общее событие" name="CommonEvent">
<property label="Код события" name="code" type="String"></property>
<property label="Название события" name="name" type="String"></property>
<property label="Дата начала события" name="beginDate" type="Date"></property>
<property label="Дата окончания события" name="endDate" type="Date"></property>
<property label="Тип события" name="eventType" type="String"></property> <!-- для краткости применен тип String, а не справочник -->
</class>
<class label="Организация" name="Organization">
<property label="Название организации" name="name" type="String"></property>
<property mappedby="orgId" name="ogrEvents" type="OrgEvent"></property>
</class>
<class implements="Event" label="Событие организации" name="OrgEvent">
<property label="Код события" name="code" type="String"></property>
<property label="Название события" name="name" type="String"></property>
<property label="Дата начала события" name="beginDate" type="Date"></property>
<property label="Дата окончания события" name="endDate" type="Date"></property>
<property label="Организация, которой принадлежит событие" name="orgId" parent="true" type="Organization"></property>
<property label="доп. код события" name="subCode" type="String"></property>
</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);
note

В интерфейс можно выносить не только поля с примитивными типами, но и поля, имеющие объектовый тип или поля коллекции.

note

Все классы, имплементирующие интерфейс, должны сохранять название и тип атрибутов из интерфейса (нельзя изменить название атрибута при имплементации интерфейса).

Разыменование внешних ссылок

Агрегатоцентричная модель DataSpace предполагает разделение модели потребителя по непересекающимся агрегатам. Связь элементов, принадлежащих разным агрегатам, осуществляется через так называемые «внешние ссылки». Имеется возможность разыменования таких ссылок.

Пример того, каким образом внешние ссылки можно разыменовать, показан в следующем фрагменте кода:

// Предположим, есть сущность Request, у которой имеется внешняя ссылка на Product с именем product
// Запрос с разыменованием этой внешней ссылки
GraphCreator.selectRequest()
.withCode()
// Перегруженный метод принимает лямбду, в которой можно задавать спецификацию для разыменованной ссылки
.withProduct(productWithLinkable -> productWithLinkable.withCode().withName())
// В ограничивающем условии на объекте с внешней ссылкой появляется "виртуальное" (в плане отсутствия в БД) поле с именем entity,
// по которому можно построить условие
.setWhere(where -> where.product().entity().codeEq("someCode"));

Чтение данных порциями

Если требуется разделить читаемые данные на независимые порции (batch), можно воспользоваться методом получения хэш от значения поля. Затем к данному значению применяется операция деления с остатком на необходимое количество порций (mod).

note

На разных базах данных хэш от одного и того же значения может быть разным. Однако, при использовании хэш и остатка от деления на одной БД одна порция данных никогда не пересечется с другой. Метод 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()));

Историцирование (получение данных)

Работать с историческими данными можно посредством традиционных поисковых запросов, а также при помощи 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_idsysHistoryTimesysHistoryOwnersysHistNumbersysStatecodesysCodeUpdatednamesysNameUpdated
8654232155466872021-10-05T18:45:38.785412Z884321697854135251'someCode'truenullnull

Ограничения:

  • Отсутствует возможность получения истории связанных сущностей (вложенных сущностей). Имеется возможность получить исторические данные для каждой из связанных (вложенных) сущностей отдельно.
  • Отсутствует возможность историцирования blob-, clob-полей, поля объектов embedded, обратных ссылок и любых коллекционных полей.

Получение данных историцирования через клиента (DataspaceCoreHistoryClient)

Термины:

  • Состояние сущности – наличие значений для всех запрошенных атрибутов сущности.
  • Изменение сущности – отдельная строка в таблице изменений по соответствующей сущности. Значения имеются только у изменившихся атрибутов, у остальных – «null».

Поисковый клиент DataspaceCoreHistoryClient предоставляет следующие API:

  • Получение состояния сущности на заданный момент времени.
  • Получение списка состояний сущности за период времени (по одному состоянию на каждое изменение).
  • Получение списка изменений сущности за период времени (getHistory). Данная API может быть заменена традиционным поисковым запросом.
Определение запрашиваемых данных

Для описания запрашиваемых исторических данных необходимо воспользоваться классом с именем \[ModelClassName\]HistoryGraph, на котором вызвать метод .createCollection(). После этого необходимо вызвать with-методы, соответствующие запрашиваемым полям. Например, для запроса поля Code необходимо вызвать метод .withCode().

Для запроса признака обновленности поля необходимо вызвать метод .withSys\[PropName\]Updated. Например, для запроса признака обновленности поля Code необходимо вызвать метод .withSysCodeUpdated().

danger

Признак Updated для записей старых значений (sysState = 3) не означает, что поле было обновлено, а служит признаком установленного значения, чтобы отделить значение поля «null» от null, как отсутствия данных. Для такой записи невозможно определить, изменялось ли поле, так как на соответствующий ей момент данные поля еще не историцировались и изменения по ним не отслеживались.

Для запроса признака успешности вычисления поля необходимо вызвать метод .withSys\[PropName\]Calculated. Например, для запроса признака успешности определения значения поля Code необходимо вызвать метод .withSysCodeCalculated().

note

Признак успешности вычисления позволяет отличить действующие значения «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.

danger

Несмотря на то, что объект HistoryGraph позволяет вызвать дополнительные методы (например, для установки фильтрации .setWhere() или сортировки .setSortingAdvanced()), вызов таких методов не допускается в запросах, которые будут использованы при вызове исторических API. Сортировка в исторических API не допускается, а сортировка реализована через параметры вызова API.

Получение состояния сущности на заданный момент времени

Данный API возвращает информацию о значении всех указанных в запросе историцируемых атрибутов указанного объекта на заданный момент времени (если информации в истории достаточно для их расчета).

Сигнатура API в SDK: Optional&lt;&lt;EntityName&gt;HistoryGet&gt; dataspaceCoreHistoryClient.&lt;EntityName&gt;HistoryState(&lt;EntityName&gt;HistoryCollectionWith propReq, String entityId, OffsetDateTime time) throws SdkJsonRpcClientException, где:

  • propReq – запрос историцируемых свойств. Такой же объект, как и при обычных поисках, при этом он должен содержать только описание запрашиваемых полей. Расчетные поля, условия фильтрации и сортировки не допускаются.
  • entityId – идентификатор сущности, для которой требуется определить состояние.
  • time – время, на которое требуется определить состояние (обязательный параметр).

Если API не удалось рассчитать ни одного запрошенного поля, то возвращается null. Если API удается рассчитать хотя бы одно поле, то возвращается результат, при этом поля, для которых не удалось рассчитать значение, будут содержать 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.

Обязательными для выполнения функции являются следующие параметры: идентификатор объекта, время начала и конца периода поиска данных.

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) – устанавливает направление сортировки данных в прямом или обратном хронологическом порядке (по умолчанию – прямой порядок).

Пример вызова 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&lt;&lt;EntityName&gt;HistoryGet&gt; historyTestingHistory(&lt;EntityName&gt;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)
);
Поиск по историческим данным

Если какой-либо атрибут сущности помечен историцируемым, то для всей цепочки наследования (вверх и вниз по иерархии) создаются исторические классы. Названия исторического класса формируется путем добавления суффикса 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 + "%"))
<