Протокол JSON-RPC#

Введение#

Компонент DataSpace Core предоставляет потребителю возможность вызывать основные функции продукта Platform V DataSpace по протоколу JSON-RPC 2.0.

Общие сведения#

Настройки по умолчанию:

  • URL-адрес контроллера пакета изменений: {URL}:8080/packet.

  • URL-адрес контроллера поиска: {URL}:8080/search.

  • URL-адрес контроллера запросов историцирования: {URL}:8080/history.

  • Авторизация отключена.

Продукт DataSpace позволяет работать с сущностями реализованной предметной модели. Способы реализации объектной модели Продукта можно найти в документе "Руководство по ведению модели данных".

Для работы с данными модели используются три JSON-RPC контроллера:

  • Контроллер пакета изменений — обеспечивает атомарное изменение связанных сущностей.

  • Контроллер поиска — обеспечивает поиск сущностей по условиям.

  • Контроллер запросов историцирования — обеспечивает поиск и агрегацию историчных данных.

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

Протокол JSON-RPC 2.0 в применении к контроллерам модуля#

Согласно спецификации протокола JSON-RPC 2.0 запрос должен иметь определения для:

  • id: целочисленный идентификатор запроса;

  • method: имя вызываемого метода;

  • params: параметры вызываемого метода.

Контроллеры модуля имеют единственный метод execute с объектным параметром packet для контроллера пакета изменений, и объектным параметром request для контроллера поиска.

Структура запроса контроллера пакета изменений:

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": 1,
  "params": {
    "packet": {
      
    }
  }
}

Состав объекта packet рассматривается в разделе Контроллер пакета изменений.

Структура запроса для контроллера поиска:

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": 1,
  "params": {
    "request": {
      
    }
  }
}

Состав объекта request рассматривается в разделе Контроллера поиска.

Структура положительного ответа:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
      
  }
}

Состав объекта result зависит от спецификации запроса.

Структура отрицательного ответа:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32099,
    "message": "сообщение_об_ошибке",
    "data": "опциональная_детализация_ошибки"
  }
}

Состав:

  • code: код согласно спецификации JSON-RPC;

  • message: текст сообщения;

  • data: дополнительная информация об ошибке.

Список кодов ошибок:

  • OBJECT_NOT_FOUND: сущность с указанным идентификатором не найдена;

  • PARSE_ERROR: ошибка разбора запроса;

  • INVALID_ARGUMENT: ошибка аргумента в запросе;

  • DATA_ACCESS: ошибка уровня базы данных;

  • DATA_ACCESS_CONSTRAINT: ошибка уровня базы данных о нарушении ограничения;

  • IDEMPOTENCY_EXCEPTION: ошибка идемпотентного вызова;

  • STATUS_EXCEPTION: ошибка статуса;

  • AGGREGATE_EXCEPTION: ошибка использования агрегата;

  • AGGREGATE_VERSION_EXCEPTION: ошибка версии агрегата;

  • SYSTEM_LOCK_EXCEPTION: ошибка системной блокировки;

  • APPLICATION_LOCK_EXCEPTION: ошибка прикладной блокировки;

  • MASK_NOT_MATCH_EXCEPTION: ошибка проверки значения маске;

  • COMPARE_NOT_EQUAL: ошибка сравнения значений;

  • HISTORY_EXCEPTION: ошибка истории;

  • READ_RECORDS_COUNT_EXCEEDED_LIMIT_EXCEPTION: ошибка превышения лимита чтения;

  • FOREIGN_KEY: ошибка удаления сущности при нарушении целостности контролируемых внешних ссылок (integrity-check);

  • TOO_MANY_RESULTS: ошибка чтения сущности командой get при получении более одной записи по условию;

  • INC_FAIL_EXCEPTION: ошибка нарушения допустимого значения при использовании inc.

Структура множественного (batch) запроса:

[
  {
    "jsonrpc": "2.0",
    "method": "execute",
    "id": 1,
    "params": {
      "packet": {
        
      }
    }
  },
  {
    "jsonrpc": "2.0",
    "method": "execute",
    "id": 2,
    "params": {
      "packet": {
        
      }
    }
  }
]

Множественный (batch) запрос выполняется контроллером параллельно. То есть каждый элемент массива запроса рассматривается как отдельный вызов. Для контроллера пакета изменений атомарным будет считаться каждый отдельный массив запроса, а не весь вызов в целом.

Структура ответа множественного вызова:

[
  {
    "jsonrpc": "2.0",
    "id": 1,
    "error": {
      "code": -32099,
      "message": "сообщение_об_ошибке",
      "data": "опциональная_детализация_ошибки"
    }
  },
  {
    "jsonrpc": "2.0",
    "id": 2,
    "result": {
      
    }
  }
]

Модель данных используемая в примерах доступна здесь

Контроллер пакета изменений#

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

Доступные команды:

  • create: команда создания сущности;

  • updateOrCreate: изменение или создание сущности;

  • update: изменение сущности;

  • delete: удаление сущности;

  • tryLock: прикладная блокировка сущности;

  • unlock: снятие прикладной блокировки с сущности;

  • get: чтение сущности.

Примечание

Далее в примерах будет опускаться типовой конверт JSON-RPC для концентрации на следующих объектах из его состава:

  • packet: для запросов контроллера пакета изменений;

  • result: для описания успешного выполнения запроса контроллера пакета изменений или поиска;

  • error: для описания ошибочного выполнения запроса контроллера пакета изменений или поиска.

Структура запроса#

Команды пакета указываются в атрибуте commands параметра JSON-RPC запроса packet. Структура параметра packet с массивом команд:

{
  "commands": []
}

Элементами массива являются объекты команд. Общая структура объекта команды:

{
  "id": "идентификатор_команды",
  "name": "имя_команды",
  "params": {
    "type": "тип_сущности",
    "id": "идентификатор_сущности"
  }
}

Описание:

  • id: уникальный идентификатор команды в пакете. Если атрибут не указан, то в качестве идентификатора команды используется позиция в массиве commands. Первый элемент массива имеет индекс "0".

  • name: обязательный атрибут имени команды.

  • params: обязательный атрибут блока параметров команды:

    • type: обязательный атрибут названия типа сущности.

    • id: атрибут идентификатора сущности:

      • обязательный для команд update, delete, tryLock, unlock, get;

      • для команд create, updateOrCreate доступность и обязательность зависит от типа сущности (детали в описании команды create).

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

Структура ответа#

Каждая команда имеет специфичный для нее результат:

  • create: идентификатор созданной сущности;

  • updateOrCreate: идентификатор созданной/измененной сущности и признак создания;

  • update, delete: фиксированный ответ void;

  • tryLock, unlock: объект с информацией об установке/снятии прикладной блокировки;

  • get: объект с проекцией запрошенных атрибутов.

Детальная информация представлена в описании команд.

При успешном выполнении пакета объект result содержит атрибут commands, включающий результаты команд пакета. Порядок следования результатов соответствует порядку следования команд в пакете.

Пример объекта packet запроса:

{
  "commands": [
    {
      "id": "createProduct",
      "name": "create",
      "params": {
        "id": "1",
        "type": "Product"
      }
    },
    {
      "id": "updateProduct",
      "name": "update",
      "params": {
        "id": "1",
        "type": "Product"
      }
    }
  ]
}

Пример объекта result ответа:

{
  "commands": [
    "1",
    "void"
  ]
}

Существует возможность представления результата в ином виде. Для этого в объекте packet необходимо определить атрибут commandsResponseMode с одним из следующих значений:

  • ARRAY: атрибут commands ответа представлен массивом, это значение по умолчанию;

  • OBJECT: атрибут commands ответа представлен объектом, атрибутами которого являются идентификаторы команд;

  • OBJECT_NO_VOID: аналогично OBJECT с исключением команд, имеющих результат void.

Пример запроса с результатом OBJECT:

{
  "commandsResponseMode": "OBJECT",
  "commands": [
    {
      "id": "createProduct",
      "name": "create",
      "params": {
        "id": "1",
        "type": "Product"
      }
    },
    {
      "id": "updateProduct",
      "name": "update",
      "params": {
        "id": "1",
        "type": "Product"
      }
    }
  ]
}

Ответ:

{
  "commands": {
    "createProduct": "1",
    "updateProduct": "void"
  }
}

Пример запроса с результатом OBJECT_NO_VOID:

{
  "commandsResponseMode": "OBJECT_NO_VOID",
  "commands": [
    {
      "id": "createProduct",
      "name": "create",
      "params": {
        "id": "1",
        "type": "Product"
      }
    },
    {
      "id": "updateProduct",
      "name": "update",
      "params": {
        "id": "1",
        "type": "Product"
      }
    }
  ]
}

Ответ:

{
  "commands": {
    "createProduct": "1"
  }
}

Создание связанных сущностей#

В рассмотренном ранее примере идентификатор сущности известен заранее, т.е. не генерируется модулем. Если идентификатор является генерируемым, то значение можно получить только после выполнения пакета. Чтобы иметь возможность использовать генерируемый идентификатор в качестве значения атрибута, применяется конструкция ref:идентификатор_создающей_сущность_команды. Пример:

{
  "commands": [
    {
      "id": "createProduct",
      "name": "create",
      "params": {
        "type": "Product"
      }
    },
    {
      "id": "createPerformedService",
      "name": "create",
      "params": {
        "type": "PerformedService",
        "product": "ref:createProduct"
      }
    }
  ]
}

Пакет создает сущность типа Product и сущность типа PerformedService, которая по модели имеет родительскую связь с Product через атрибут product. В данном случае Product определяет автоматическую генерацию идентификатора, и для определения значения этого идентификатора при создании связанной сущности PerformedService применена конструкция ref:createProduct, т.е. указан идентификатор команды, создающей родительскую сущность.

Идемпотентный пакет#

Модуль предоставляет возможность предотвращения повторной обработки пакета на основе инструмента идемпотентности.
Чтобы сделать пакет идемпотентным, необходимо добавить атрибут idempotencePacketId, значение которого определяет глобальную уникальность пакета.

Краткое описание алгоритма:

  1. Выполняется поиск записи в системной таблице идемпотентных вызовов, связанных с агрегатом пакета для значения idempotencePacketId.

  2. Если нет предыдущего вызова, то создается запись с регистрацией хеш входящих параметров запроса и результатов выполнения. Если предыдущий вызов есть:

    1. Проверяется совпадение хеш входящих параметров текущего запроса с предыдущим.

    2. Для команд пакета create, updateOrCreate, update и delete используется результат предыдущего вызова (т.е. команды не исполняются).

    3. Команды get, tryLock и unlock исполняются в обычном режиме.

Пример не идемпотентного пакета:

{
  "commands": [
    {
      "name": "create",
      "params": {
        "type": "ProductParty"
      }
    }
  ]
}

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

{
  "commands": [
    "6829272897192919041"
  ]
}

Пример идемпотентного вызова:

{
  "idempotencePacketId": "PACKET_CALL_UNIQUE_ID",
  "commands": [
    {
      "name": "create",
      "params": {
        "type": "ProductParty"
      }
    }
  ]
}

Результат выполнения содержит ссылку созданного объекта типа ProductParty:

{
  "commands": [
    "6829273823110692865"
  ]
}

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

{
  "isIdempotenceResponse": true,
  "commands": [
    "6829273823110692865"
  ]
}

Значение "true" атрибута isIdempotenceResponse указывает на повторное исполнение идемпотентного пакета, в противном случае атрибут отсутствует.

Оптимистическая блокировка#

Для поддержки оптимистической блокировки агрегата в пакете определено опциональное поле aggregateVersion, значение которого зависит от варианта использования:

  • Запрос версии агрегата: значение — "-1". В результате выполнения пакета будет возвращена текущая версия агрегата. Проверка версии объектов при модификации данных выполняться не будет.

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

    • В идемпотентном пакете проверка версии не выполняется, в результате возвращается актуальная версия агрегата.

    • С пакетом, не содержащим команд модификации (т.е. используются только команды get), допускается только запрос версии агрегата.

Значение версии агрегата в результате выполнения пакета находится в поле aggregateVersion. Пример ответа пакета с версией:

{
  "aggregateVersion": "3",
  "commands": [
    "void"
  ]
}

Внимание!

Оптимистическая блокировка не допускается к использованию в пакете более чем с одним агрегатом. Исключение составляет читающий пакет, в этом случае версия агрегата определяется по первой команде.

Контроль значений BigDecimal#

Модуль выполняет проверку переданных в пакете команд значений типа BigDecimal на соответствие заданных в модели length и scale. Имеется три варианта работы проверки, которые определяются параметром модуля dataspace.types.normalizer.decimalPrecisionCheck:

  • COMPATIBILITY: проверка не осуществляется, значение может быть округлено на уровне БД, при этом в векторе изменений будет исходное значение;

  • STRICT: переданное значение должно соответствовать параметрам модели, в противном случае генерируется ошибка INVALID_ARGUMENT;

  • TRUNCATE: дробная часть обрезается до указанного в модели значения.

Модель для демонстрации:

    <class name="Sample">
        <id category="AUTO_ON_EMPTY"/>
        <property name="bigDecimal" type="BigDecimal" length="4" scale="2"/>
    </class>

Тестовый пакет json-rpc:

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": 1,
  "params": {
    "packet": {
      "commands": [
        {
          "name": "create",
          "params": {
            "type": "Sample",
            "id": "42",
            "bigDecimal": "12.345"
          }
        },
        {
          "name": "get",
          "params": {
            "type": "Sample",
            "id": "42",
            "props": "bigDecimal"
          }
        }
      ]
    }
  }
}

Для варианта STRICT (дефолтный) выполнение пакета завершится ошибкой:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32091,
    "message": "Ошибка обработки команды id = '0', name = 'create': Ошибка обработки поля 'bigDecimal': Длина значения 5 превышает допустимое 4",
    "data": "INVALID_ARGUMENT"
  }
}

Для варианта COMPATIBILITY выполнение пакета будет успешным:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "commands": [
      "42",
      {
        "type": "Sample",
        "id": "42",
        "props": {
          "bigDecimal": "12.35"
        }
      }
    ]
  }
}

Значение будет округлено базой данных, в векторе изменений содержится исходное 12.345.

Для варианта TRUNCATE выполнение пакета будет успешным:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "commands": [
      "42",
      {
        "type": "Sample",
        "id": "42",
        "props": {
          "bigDecimal": "12.34"
        }
      }
    ]
  }
}

Значение в дробной части будет обрезано в соответствии с настройкой модели до 12.34, в векторе изменений содержится 12.34.

Описание команд пакета#

В описании используется детализация структуры объекта команды.

Команда создания сущности create#

Команда create создает сущность для указанного типа.

Сигнатура команды:

{
  "name": "create",
  "params": {
    "type": "тип_сущности",
    "id": "идентификатор_сущности",
    "имя_свойства_сущности": "значение_атрибута"
  }
}

Описание:

  • type: обязательный атрибут имени типа сущности;

  • id: использование атрибута идентификатора сущности описано далее;

  • имя_свойства_сущности: атрибуты устанавливаемых при создании свойств создаваемой сущности.

Результат команды: строка, содержащая идентификатор созданной сущности.

Объект params расширяется атрибутами типа модели.

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

Атрибут идентификатора сущности id

Особенности атрибута:

  • обязательный с типом сущности, определяющим категорию идентификатора MANUAL;

  • опциональный с типом сущности, определяющим категорию идентификатора AUTO_ON_EMPTY или UUIDV4_ON_EMPTY;

  • отсутствует с типом сущности, определяющим категорию идентификатора AUTO (SNOWFLAKE) или UUIDV4.

Скалярные свойства

Имена атрибутов соответствуют именам свойств типа в модели, значение форматируется в соответствии представлением java-типа свойства модели в json:

  • свойства типа LocalDate принимают дату в формате yyyy-MM-dd;

  • свойства типа LocalDateTime и Date принимают дату в формате yyyy-MM-dd'T'hh:mm:ss.SSS;

  • свойства типа enum используют значения атрибута name описания enum в модели;

  • коллекционное свойство заполняется массивом значений. При сохранении в сущность коллекция полностью заменяется.

Вложенные типы (embeddable)

Атрибут свойства вложенного типа должен быть объектом, атрибуты которого в свою очередь соответствуют свойствам вложенного типа.

Пример модели:

<model>
  <class name="Address" embeddable="true">
    <property name="city" type="String"/>
    <property name="street" type="String"/>
  </class>
  <class name="Order">
    <property name="address" type="Address"/>
  </class>
</model>

Пример команды:

{
  "name": "create",
  "params": {
    "type": "Order",
    "address": {
      "city": "Безымянный",
      "street": "Главная"
    }
  }
}

Внешняя ссылка

Атрибут свойства внешней ссылки должен быть объектом, атрибутный состав которого зависит от варианта внешней ссылки:

  • ссылка на внешний по отношению к модели тип или на корневую сущность агрегата должна иметь обязательный атрибут entityId;

  • ссылка на элемент агрегата должна иметь обязательные атрибуты entityId и rootEntityId (идентификатор агрегата владельца сущности).

Пример:

{
  "name": "create",
  "params": {
    "type": "Deposit",
    "owner": {
      "entityId": "external_client_identifier"
    },
    "depositAccount": {
      "entityId": "DepositAccount_identifier",
      "rootEntityId": "DepositOwner_identifier"
    }
  }
}

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

Атрибут свойства коллекции внешних ссылок должен быть объектом со следующими атрибутами:

  • clear: тип значения boolean, определяет необходимость очистки коллекции;

  • add: тип значения массив объектов добавляемых внешних ссылок;

  • remove: тип значения массив объектов исключаемых внешних ссылок.

Статусы

Если в модели для типа сущности определены статусы, то становится доступен атрибут с именем statusForНаблюдатель, где Наблюдатель — код наблюдателя статуса. Атрибут должен являться объектом со следующими атрибутами:

  • code: обязательный код устанавливаемого статуса;

  • reson: опциональная причина установки статуса.

Пример:

{
  "name": "create",
  "params": {
    "type": "Product",
    "statusForService": {
      "code": "depositOpened",
      "reason": "Открыт новый депозит"
    }
  }
}

Ссылки

Свойства ссылочного типа должны иметь значение типа String с указанием идентификатора сущности, на которую они ссылаются. Значение может быть ссылкой на команду ref:идентификатор_команды, описание которого приведено ранее. Свойства, являющиеся коллекциями внешних ссылок, не используются в атрибутах команды.

Обязательные поля

При заполнении атрибутов команды create свойства типа с атрибутом mandatory="true" являются обязательными к заполнению. Кроме этих явно указанных свойств обязательным является ссылка на родительскую сущность (parent="true) и свойства, входящие в состав индексов CCI.

Если происходит изменение атрибута mandatory поля, то правила следующие:

  • Для примитивных типов и перечислений помимо требования заполнить значение поля при создании/обновлении объекта производится заполнение обязательных полей существующих объектов в БД на этапе применения Liquibase-скриптов. Значение для заполнения существующих данных указывается через значение атрибута default-value.

  • Для ссылок (в том числе справочников) и мягких ссылок значение default-value не указывается. Изменение атрибута mandatory для таких полей приведет к требованию обязательного заполнения при создании/обновлении сущности. Заполнить данные можно либо обновлением сущностей стандартным средством обновления через команды или graphQl, либо воспользоваться персонализированным changelog-ом (см. раздел "Создание персонализированного changelog" документа "Порядок внесения изменений в модель данных").

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

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

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

  • категория идентификатора MANUAL;

  • категория идентификатора AUTO_ON_EMPTY или UUIDV4_ON_EMPTY;

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

Примечание

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

Сигнатура команды:

{
  "name": "updateOrCreate",
  "params": {
    "type": "тип_сущности",
    "id": "идентификатор_сущности",
    "имя_свойства_сущности": "значение_атрибута"
  },
  "exist": {
    "byKey": "имя_уникального_индекса"
  }
}

Описание:

  • type: обязательный атрибут имени типа сущности;

  • id: использование атрибута идентификатора сущности зависит от типа и аналогично команде create;

  • имя_свойства_сущности: атрибуты устанавливаемых при создании или изменяемых свойств создаваемой/изменяемой сущности;

  • exist: объект с атрибутом byKey, содержащим имя уникального индекса.

Отношение id и exist

Параметр params команды заполняется по правилам create, т.е. использование атрибута id зависит от категории идентификатора. Для категории AUTO_ON_EMPTY или UUIDV4_ON_EMPTY атрибут является опциональным. Логика команды требует определенного критерия, который однозначно идентифицирует сущность. Если в params заполнен атрибут id, то поиск выполняется по значению этого атрибута. Если атрибут id не используется, то обязательно должен быть заполнен атрибут byKey объекта exist, содержащий имя уникального индекса. Значения для поиска определяются следующим образом:

  1. Используется значение атрибута в params для свойства из состава индекса.

  2. Если атрибут не указан, то используется значение по умолчанию для поля.

  3. В противном случае — значение "null".

Результат команды:

{
  "id": "идентификатор_сущности",
  "created": "логический_признак_создания_сущности_командой"
}

Описание:

  • id: идентификатор созданной или измененной сущности;

  • created: логический атрибут принимает значение "true", если объект создан, или "false" для существующего экземпляра.

Поиск по уникальному индексу

Модель технической сущности для демонстрации:

<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.

Команда с использованием пользовательского значения уникального идентификатора:

{
  "name": "updateOrCreate",
  "params": {
    "type": "SampleEntity",
    "id": "42"
  }
}

При выполнении команды используется значение уникального идентификатора.

Команда с поиском по уникальному индексу:

{
  "name": "updateOrCreate",
  "params": {
    "type": "SampleEntity",
    "altKey": "KEY-42"
  },
  "exist": {
    "byKey": "altKey"
  }
}

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

Пример модели сущности с несколькими составными индексами:

<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"/>
    <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.

Частичное обновление сущности

Выполнение команды для существующей сущности приводит к изменению текущих значений полей на указанные в параметрах команды. Частичное обновление полей можно выполнить путем добавления в параметр команды exist объекта с именем update, в котором указываются необходимые к изменению поля и их значения.

Модель технической сущности для демонстрации:

<model>
    <class name="SampleEntity">
        <id category="AUTO_ON_EMPTY"/>
        <property name="code" type="String"/>
        <property name="name" type="String"/>
        <property name="altKey" type="String" unique="true"/>
    </class>
</model>

Пример структуры команды частичного обновления:

{
  "name": "updateOrCreate",
  "params": {
    "type": "Sample",
    "id": "42",
    "code": "initial code",
    "name": "initial name"
  },
  "exist": {
    "update": {
      "name": "name after partial updateOrCreate"
    }
  }
}

Пояснение:

  • Если сущность с идентификатором 42 не существует, то после выполнения команды значения полей code и name будут иметь значения initial code и initial name соответственно.

  • Если сущность с идентификатором 42 существует, то после выполнения команды значение поля name изменится на name after partial updateOrCreate, а поле code будет иметь значение на момент выполнения команды.

Если при выполнении команды не требуется выполнять обновление, то необходимо объект update параметра exist представить пустым объектом:

{
  "name": "updateOrCreate",
  "params": {
    "type": "Sample",
    "id": "42",
    "code": "initial code",
    "name": "initial name"
  },
  "exist": {
    "update": { }
  }
}

Или null:

{
  "name": "updateOrCreate",
  "params": {
    "type": "Sample",
    "id": "42",
    "code": "initial code",
    "name": "initial name"
  },
  "exist": {
    "update": null
  }
}

Команда изменения сущности update#

Структура команды update:

{
  "name": "delete",
  "params": {
    "type": "тип_сущности",
    "id": "идентификатор_сущности",
    "имя_изменяемого_свойства_сущности": "новое_значение_атрибута"
  },
  "compare": {
    "имя_сравниваемого_свойства_сущности": "ожидаемое_значение"
  },
  "inc": {
    "имя_инкриминируемого_свойства_сущности": {
      "value": "значение_инкремента"
    }
  }
}

Описание:

  • type: обязательный атрибут имени типа сущности;

  • id: обязательный атрибут идентификатора сущности;

  • имя_изменяемого_свойства_сущности: атрибуты изменяемых свойств, правила заполнения аналогичны команде create;

  • compare: опциональный объект для проверки текущих значений свойств сущности ожидаемым;

  • inc: опциональный объект для изменения текущих значений свойств сущности на указанную в value для конкретного свойства.

Результат команды: void.

Изменение свойства parent

Ключевым элементом определения дерева агрегата является свойство класса с признаком parent="true". Команда допускает изменение значения в границах текущего агрегата сущности.

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

Для свойств сущности с типами String, Integer, Long, Date, LocalDate, LocalDateTime, OffsetDateTime можно определить проверку на соответствие фактических значений сущности ожидаемым. Если по одному из указанных свойств значение не совпадает, то команда завершится ошибкой. Проверка выполняется до внесения изменений по свойствам.

Пример:

{
  "commands": [
    {
      "name": "create",
      "params": {
        "type": "SampleEntity",
        "code": "sample code",
        "name": "sample name"
      }
    },
    {
      "name": "update",
      "props": {
        "type": "SampleEntity",
        "id": "ref:0",
        "code": "new sample code",
        "name": "new sample name"
      },
      "compare": {
        "code": "sample code",
        "name": "wrong sample name"
      }
    }
  ]
}

Результат выполнения пакета будет содержать ошибку:

{
  "code": -32095,
  "message": "Ошибка обработки команды id = '1', name = 'update': Ошибка обработки сравниваемого поля 'name': Расхождение ожидаемого 'wrong sample name' и фактического 'sample name' значений",
  "data": "COMPARE_NOT_EQUAL"
}

Ошибка связана с тем, что команда update согласно значениям в compare ожидает, что свойство code должно быть эквивалентным sample code, а свойство name должно быть эквивалентным wrong sample name.

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

Для свойств сущности с типами Integer, Long, Float, Double, BigDecimal можно выполнить операцию инкремента текущего значения на указанное в параметрах команды. Передаваемое значение может быть отрицательным для выполнения операции декремента.

Пример:

{
  "commands": [
    {
      "id": "0",
      "name": "create",
      "params": {
        "type": "SampleEntity",
        "sum": "3.14",
        "counter": 9
      }
    },
    {
      "id": "1",
      "name": "update",
      "params": {
        "id": "ref:0",
        "type": "SampleEntity"
      },
      "inc": {
        "sum": {
          "value": "42"
        },
        "counter": {
          "value": -4
        }
      }
    },
    {
      "id": "2",
      "name": "get",
      "params": {
        "id": "ref:0",
        "type": "SampleEntity",
        "props": [
          "sum",
          "counter"
        ]
      }
    }
  ]
}

Результат выполнения пакета:

{
  "commands": [
    "7142451581592076289",
    "void",
    {
      "id": "7142451581592076289",
      "type": "SampleEntity",
      "props": {
        "sum": "45.14",
        "counter": "5"
      }
    }
  ]
}

Операция выполняется после применения значений, переданных в params.

Существует возможность проверки на соответствие вычисленного значения условию. Если условие выполняется, то новое значение поля является ошибочным, и будет сформировано исключение уровня пакета INC_FAIL_EXCEPTION. Условие определяется добавлением поля fail в объект описания inc для поля сущности. Описание объекта fail:

{
  "operator": ..., 
  "value": ...
}

Описание:

  • operator: обязательное не пустое строковое поле логического оператора сравнения. Допустимые значения:

    • lt — меньше (less than);

    • le — меньше или равно (less or equal);

    • gt — больше (greater);

    • ge — больше или равно (greater or equal).

  • value: обязательное не пустое числовое поле со значением которого сравнивается вычисленное значение.

Пример объекта для проверки меньше нуля:

{
  "operator": "lt",
  "value": 0
}

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

{
  "commands": [
    {
      "id": "0",
      "name": "create",
      "params": {
        "type": "SampleEntity",
        "sum": "3.14"
      }
    },
    {
      "id": "1",
      "name": "update",
      "params": {
        "type": "SampleEntity",
        "id": "ref:0"
      },
      "inc": {
        "sum": {
          "value": "-5",
          "fail": {
            "operator": "lt",
            "value": "0"
          }
        }
      }
    }
  ]
}

Результат выполнения:

{
  "jsonrpc": "2.0",
  "id": "1",
  "error": {
    "code": -32076,
    "message": "Ошибка обработки команды id = '1', name = 'update': Ошибка обработки инкриминируемого поля 'sum': Новое значение поля '-1.86', полученное после сложения с '-5', нарушает ограничение 'LESS 0'",
    "data": "INC_FAIL_EXCEPTION"
  }
}

Описание примера:

  • первая команда создает экземпляр класса SampleEntity со значением 3.14 в поле sum;

  • вторая команда через параметр inc выполняет изменение значения поля sum на -5, то есть вычисленное значение 3.14 - 5 = -1.86. Параметр inc содержит объект fail с условием меньше нуля ("operator": "lt", "value": 0), при выполнении которого формируется ошибка INC_FAIL_EXCEPTION.

Команда удаления сущности delete#

Структура команды delete:

{
  "name": "delete",
  "params": {
    "type": "тип_сущности",
    "id": "идентификатор_сущности"
  },
  "compare": {
    "имя_сравниваемого_параметра_сущности": "ожидаемое_значение"
  }
}

Описание:

  • type: обязательный атрибут имени типа сущности;

  • id: обязательный атрибут идентификатора сущности;

  • compare: опциональный объект для проверки текущих значений свойств сущности ожидаемым (аналогично команде update).

Результат команды: void.

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

Работа команды get основана на поиске сущности из контроллера поиска по идентификатору. Структура команды:

{
  "name": "get",
  "params": {
    "type": "тип_сущности",
    "id": "идентификатор_сущности",
    "props": "спецификация_запроса"
  }
}

Описание:

  • type: обязательный атрибут имени типа сущности;

  • id: обязательный атрибут идентификатора сущности;

  • props: обязательный атрибут спецификации запрашиваемых свойств сущности в соответствии с описанием в контроллере поиска.

Результат выполнения команды: структура ответа соответствует объекту атрибута elems поиска сущности.

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

Пример:

{
  "commands": [
    {
      "name": "create",
      "params": {
        "type": "Product",
        "name": "name after create"
      }
    },
    {
      "name": "get",
      "props": {
        "type": "Product",
        "id": "ref:0",
        "props": "name"
      }
    },
    {
      "name": "update",
      "props": {
        "type": "Product",
        "id": "ref:0",
        "name": "name after update"
      }
    },
    {
      "name": "get",
      "props": {
        "type": "Product",
        "id": "ref:0",
        "props": "name"
      }
    }
  ]
}

Результат выполнения пакета:

{
  "commands": [
    "7044827754648961025",
    {
      "type": "Product",
      "id": "7044827754648961025",
      "props": {
        "name": "name after create"
      }
    },
    "void",
    {
      "type": "Product",
      "id": "7044827754648961025",
      "props": {
        "name": "name after update"
      }
    }
  ]
}

Итоговое значение поля name сущности после выполнения пакета равно name after update, но результат выполнения команды get после команды create содержит промежуточное значение этого поля.

Блокирующее чтение

При необходимости выполнять блокирующее чтение сущности уровня БД (select for update). Вариант блокировки определяется полем lock объекта params. Пример пакета:

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": 1,
  "params": {
    "packet": {
      "commands": [
        {
          "name": "get",
          "params": {
            "type": "Sample",
            "id": "42",
            "props": "code",
            "lock": "WAIT"
          }
        }
      ]
    }
  }
}

Возможные значения lock:

  • NOT_USER: блокировка не выполняется, отсутствие поля lock в команде соответствует этому значению;

  • WAIT: блокировка ожидания освобождения ресурса;

  • NOWAIT: для блокированной другой транзакцией записи пакет будет завершен ошибкой DATA_ACCESS с текстом сообщения зависящим от используемого драйвера БД.

Чтение по условию

Базовое поведение команды get позволяет выполнить чтение сущности по ее идентификатору, в случае отсутствия сущности формируется ошибка OBJECT_NOT_FOUND. Существует возможность чтения сущности по определенному в формате [строковых выражений][104] условию. Результат условия должен обеспечивать получение единственной записи или их отсутствие. Если по условию найдено несколько сущностей, то будет сформирована ошибка TOO_MANY_RESULTS. Отсутствие записи не приведет к формированию ошибки, в ответе пакета результат выполнения команды будет представлен пустим объектом. Условие поиска указывается в поле id параметров команды params в формате строкового выражения после префикса find:. Пример пакета с созданием и чтением сущности по полю:

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": 1,
  "params": {
    "packet": {
      "commands": [
        {
          "name": "create",
          "params": {
            "type": "Sample",
            "code": "sample-code"
          }
        },
        {
          "name": "get",
          "params": {
            "type": "Sample",
            "id": "find:root.code=='sample-code'",
            "props": "code"
          }
        }
      ]
    }
  }
}

Результат выполнения пакета:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "commands": [
      "7139511462685900801",
      {
        "type": "Sample",
        "id": "7139511462685900801",
        "props": {
          "code": "sample-code"
        }
      }
    ]
  }
}

Пример пакета чтения не созданной сущности:

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": 1,
  "params": {
    "packet": {
      "commands": [
        {
          "name": "get",
          "params": {
            "type": "Sample",
            "id": "find:root.code=='undefined-sample-code'",
            "props": "code"
          }
        }
      ]
    }
  }
}

Результат:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "commands": [
      {}
    ]
  }
}

Ошибка при отсутствующей записи

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

  • по идентификатору: формируется ошибка OBJECT_NOT_FOUND;

  • поисковым условием: ошибки не формируется, результат представлен пустым объектом.

Такое поведение может быть изменено добавлением логического поля failOnEmpty в объект параметров команды params. Если значение поля false, то при пустом результате ошибка OBJECT_NOT_FOUND не формируется. Пример изменения поведения по умолчанию при поиске по идентификатору:

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": 1,
  "params": {
    "packet": {
      "commands": [
        {
          "name": "get",
          "params": {
            "type": "Sample",
            "id": "undefined-entity",
            "props": "code",
            "failOnEmpty": false
          }
        }
      ]
    }
  }
}

Результат выполнения:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "commands": [
      {}
    ]
  }
}

Внимание!

В пакете с запросом версии агрегата, состоящем только из команд get, первая команда должна гарантировать получение записи, то есть выполнять чтение по идентификатору без использования строкового условия. Поле failOnEmpty должно отсутствовать или иметь значение true.

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

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

  • попытка установки блокировки (tryLock);

  • снятие блокировки (unlock).

Параметры команды установки блокировки (tryLock):

{
  "name": "tryLock",
  "params": {
    "type": "тип_сущности",
    "id": "идентификатор_блокируемой_сущности",
    "timeout": "период_действия_блокировки_с_момента_установки_(мс)",
    "token": "токен_блокировки",
    "reason": "причина"
  }
}

Описание:

  • type: обязательный атрибут имени типа сущности;

  • id: обязательный атрибут идентификатора сущности;

  • timeout: период действия блокировки с момента установки (мс);

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

  • reason: опциональный параметр причины установки блокировки.

Результат выполнения команды:

{
  "token": "токен",
  "timeoutEndTime": "время_истечения_блокировки"
}

Описание:

  • token: сгенерированный токен блокировки;

  • timeoutEndTime: время истечения блокировки.

Параметры команды снятия блокировки (unlock):

{
  "name": "unlock",
  "params": {
    "type": "тип_сущности",
    "id": "идентификатор_сущности",
    "token": "токен"
  }
}

Описание:

  • type: обязательный атрибут имени типа сущности;

  • id: обязательный атрибут идентификатора сущности;

  • token: токен блокировки.

Результат выполнения команды:

{
  "result": "true"
}

Если команда завершена ошибкой, то пакет будет также содержать ошибку.

Команда unlock требует использование токена формируемого командой tryLock. Пример демонстрирует специальную возможность для этих команд для совмещения в одном пакете:

{
  "commands": [
    {
      "name": "create",
      "params": {
        "type": "ProductParty"
      }
    },
    {
      "name": "tryLock",
      "params": {
        "id": "ref:0",
        "type": "ProductParty",
        "timeout": 5000
      }
    },
    {
      "name": "unlock",
      "params": {
        "id": "ref:0",
        "type": "ProductParty",
        "token": "ref:1/token"
      }
    }
  ]
}

Результат выполнения пакета:

{
  "commands": [
    "7046023475444514817",
    {
      "result": true,
      "token": "917f8dca-c7f2-4af9-9970-956f3888ff2e",
      "failReason": null,
      "timeoutEndTime": 1640530185318
    },
    {
      "result": true,
      "token": "917f8dca-c7f2-4af9-9970-956f3888ff2e",
      "failReason": null,
      "timeoutEndTime": null
    }
  ]
}

Пример пакета с ошибкой прикладной блокировки:

{
  "commands": [
    {
      "name": "create",
      "params": {
        "type": "ProductParty"
      }
    },
    {
      "name": "tryLock",
      "params": {
        "id": "ref:0",
        "type": "ProductParty",
        "timeout": 5000
      }
    },
    {
      "name": "unlock",
      "params": {
        "id": "ref:0",
        "type": "ProductParty",
        "token": "wrong_token"
      }
    }
  ]
}

Результат этого пакета:

{
  "code": -32096,
  "message": "Ошибка обработки команды id = '2', name = 'unlock': Ошибка снятия прикладной блокировки ProductParty (id=7046026073245417473). Блокировка установлена другим пользователем или сервисом!",
  "data": "APPLICATION_LOCK_EXCEPTION"
}

Условное выполнение команд#

Команды пакета выполняются последовательно. Если выполняемая команда завершается ошибкой, то выполнение пакета прерывается и формируется ошибочный результат, иначе результат пакета будет содержать результат выполнения входящих в него команд. Существует возможность определить необходимость выполнения команды в пакете на основании результата выполнения другой команды (или других команд). В качестве источника результата выступают команды getи updateOrCreate.

Условие выполнения команды описывается массивом объектов dependsOn. Объект массива должен содержать поля:

  • commandId: строковый идентификатор команды источника результата;

  • dependency: строковая константа ожидаемого результата:

    • EXISTS — команда get с указанным в commandId идентификатором имеет не пустой результат;

    • NOT_EXISTS — команда get с указанным в commandId идентификатором имеет пустой результат;

    • CREATED — команда updateOrCreate с указанным в commandId идентификатором имеет значение created, равным true, то есть в результате ее выполнения была создана сущность;

    • NOT_CREATED — команда updateOrCreate с указанным в commandId идентификатором имеет значение created, равным false, то есть сущность была создана ранее.

Команда выполняется при наличии всех указанных в dependsOn условий, соответствующих true. Анализ условий в массиве выполняется до первого результата false. Команда из массива dependsOn должна следовать ранее по потоку выполнения команд пакета, то есть должна быть исполнена на момент проверки. Для команды updateOrCreate не допустим пустой результат.

Параметр dependsOn не применим к команде get.

В результате пакета пропущенные команды будут отражены пустым объектом.

Пример описания:

"commands": [
  {
    "name": "get",
    "params": {
      "type": "Sample",
      "id": "42",
      "props": "code",
      "failOnEmpty": false
    }
  },
  {
    "name": "update",
    "params": {
      "id": "42",
      "name": "new name value"
    },
    "dependsOn": [
      {
        "commandId": "0",
        "dependency": "EXISTS"
      }
    ]
  }
]

В примере команда update будет выполнена при условии существования сущности с идентификатором 42, проверка которой выполняется первой командой get.

Результат:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "commands": [
      {}
    ]
  }
}

Внимание!

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

Связывание параметров модифицирующих команд с результатом get#

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

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

<class name="SampleEntity">
    <property name="code" type="String"/>
    <property name="name" type="String"/>
</class>

Пример пакета:

{
  "commands" : [ {
    "id" : "0",
    "name" : "create",
    "params" : {
      "type" : "SampleEntity",
      "code" : "54e6e69a-9259-4890-a389-88d8e68a47ef"
    }
  }, {
    "id" : "1",
    "name" : "get",
    "params" : {
      "type" : "SampleEntity",
      "props" : "code",
      "id" : "7223331616971227137"
    }
  }, {
    "id" : "2",
    "name" : "update",
    "params" : {
      "type" : "SampleEntity",
      "id" : "7223331616971227137",
      "name" : "ref:1/props/code"
    }
  }, {
    "id" : "3",
    "name" : "get",
    "params" : {
      "type" : "SampleEntity",
      "props" : [ "code", "name" ],
      "id" : "7223331616971227137"
    }
  } ]
}

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

  1. Создается экземпляр сущности с заполнением свойства code.

  2. Для созданного экземпляра выполняется чтение свойства code.

  3. В команде update определяется необходимость заполнения свойства name значением свойства code, полученным в результате выполнения команды get.

  4. Последняя команда пакета выполняет чтение свойств code и name для подтверждения корректности работы.

Команда с "id" : "2" в свойстве name содержит значение ref:1/props/code, которое определяет:

  • ref:1 — ссылка на результат команды для использования;

  • /props/code — json-путь к значению свойства в JSON-RPC-формате ответа команды чтения.

В примере команда с "id" : "1" формирует ответ:

{
    "type" : "SampleEntity",
    "id" : "7223314278188253185",
    "props" : {
      "code" : "2f21e721-d24c-43e0-b38d-85cb2fb9d9f4"
    }
}

Из структуры ответа следует, что значение для свойства code может быть получено по пути /props/code.

Примечание

Структура ответа команды чтения зависит от структуры запроса, поэтому для связывания необходимо знать формируемый ответ в формате JSON-RPC.

Если ссылка будет вести на несуществующее поле, то пакет завершится ошибкой.

При связывании составных объектов необходимо убедиться, что структура json в ответе команды get соответствует структуре свойства команды в формате JSON-RPC.

Контроллер поиска#

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

Пример простого запроса#

Пример:

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": "1",
  "params": {
    "request": {
      "type": "PerformedService",
      "props": [
        "beginDate",
        "code"
      ],
      "offset": 15,
      "limit": 5,
      "sort": [
        {
          "crit": "root.code",
          "nullsLast": false
        },
        {
          "crit": "root.name",
          "order": "desc"
        }
      ],
      "cond": "root.code $like 'printSimpleRequestTest%'",
      "count": true,
      "aggVersion": true
    }
  }
}

Описание полей протокола:

  • type — тип искомой сущности (совпадает с именем в модели);

  • props — перечень запрашиваемых полей и их спецификаций (в данном примере запрошены примитивные поля без спецификации);

  • offset — смещение выборки, аналогично соответствующему оператору в SQL;

  • limit — ограничение на количество возвращаемых результатов;

  • sort — условие сортировки результата;

  • crit — критерий сортировки;

  • nullsLast — положение null элементов при сортировке;

  • cond — условие фильтрации (или условие отбора), ограничивающее выборку данных;

  • count — указывает на необходимость посчитать количество данных, удовлетворяющих условию фильтрации (без учета limit и offset);

  • aggVersion — указывает на необходимость вычисления и возврата версии агрегата для корневых объектов запроса.

Пример простого ответа#

Пример:

{
  "elems": [
    {
      "type": "PerformedService",
      "id": "6849277957615255557",
      "props": {
        "beginDate": "2020-07-14T13:16:35.095",
        "code": "printSimpleRequestTesttestPerformedServiceCode1"
      }
    },
    {
      "type": "PerformedService",
      "id": "6849277961910222853",
      "props": {
        "beginDate": "2020-07-14T13:16:36.021",
        "code": "printSimpleRequestTesttestPerformedServiceCode2"
      }
    }
  ],
  "count": 17
}

В ответе:

  • elems — содержит результат запроса или pod запроса;

  • type — минимальный гарантированный тип результата (реальный тип может быть потомком и должен быть запрошен явно);

  • id — идентификатор объекта;

  • props — атрибуты объекта;

  • count — количество результатов, удовлетворяющих условию фильтрации (без учета limit и offset).

Запрос вложенного объекта#

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

В примере запрашивается PerformedService, для которого запрашивается поле code (примитивного типа String) и вложенный ProductParty (ссылка Product), для которого в свою очередь запрашиваются поля code и beginDate.
Так как для запроса вложенного объекта требуется указать его спецификацию, то такие спецификации помещаются в отдельный объект внутри атрибута props вышестоящей сущности. Атрибут props может содержать только один объект, внутри которого собираются все спецификации запрошенных вложенных объектов (или их коллекций).

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": "1",
  "params": {
    "request": {
      "type": "PerformedService",
      "props": [
        "code",
        {
          "product": {
            "type": "ProductParty",
            "props": [
              "code",
              "beginDate"
            ]
          }
        }
      ]
    }
  }
}

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

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

  • cond;

  • limit;

  • offset;

  • sort.

Также для вложенной коллекции имеется возможность запросить общее количество элементов, удовлетворяющих наложенному условию фильтрации (count):

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": "1",
  "params": {
    "request": {
      "type": "PerformedService",
      "props": [
        "code",
        {
          "performedOperations": {
            "type": "PerformedOperation",
            "props": [
              "code",
              "beginDate"
            ],
            "offset": 20,
            "limit": 10,
            "sort": [
              {
                "crit": "elem.beginDate"
              }
            ],
            "cond": "elem.accountingDate>D2020-12-25T15:38:19.933",
            "count": true
          }
        }
      ]
    }
  }
}

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

Как видно из примера ниже, спецификация для вложенного объекта product и коллекции вложенных объектов performedOperations находятся в рамках одного объекта внутри атрибута props:

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": "1",
  "params": {
    "request": {
      "type": "PerformedService",
      "props": [
        "code",
        {
          "product": {
            "type": "ProductParty",
            "props": [
              "beginDate",
              "code"
            ]
          },
          "performedOperations": {
            "type": "PerformedOperation",
            "props": [
              "beginDate",
              "code"
            ],
            "offset": 20,
            "limit": 10,
            "sort": [
              {
                "crit": "elem.beginDate"
              }
            ],
            "cond": "elem.accountingDate>D2020-12-25T15:44:39.635",
            "count": true
          }
        }
      ]
    }
  }
}

Следующая спецификация запроса будет некорректной из-за того, что внутри атрибута props будет два объекта:

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": "1",
  "params": {
    "request": {
      "type": "PerformedService",
      "props": [
        "code",
        {
          "product": {
            "type": "ProductParty",
            "props": [
              "beginDate",
              "code"
            ]
          }
        },
        {
          "performedOperations": {
            "type": "PerformedOperation",
            "props": [
              "beginDate",
              "code"
            ],
            "offset": 20,
            "limit": 10,
            "sort": [
              {
                "crit": "elem.beginDate"
              }
            ],
            "cond": "elem.accountingDate>D2020-12-25T15:44:39.635",
            "count": true
          }
        }
      ]
    }
  }
}

Запрос объектов вложенного типа (embeddable)#

При запросе объекта вложенного типа (embeddable) необходимо описать запрашиваемые для него поля (спецификацию).

В примере запрашивается Request, для которого запрашивается поле code (примитивного типа String) и объект вложенного типа initiator, для которого в свою очередь запрашиваются поля firstName и lastName.

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": "1",
  "params": {
    "request": {
      "type": "Request",
      "props": [
        "code",
        {
          "initiator": [ "firstName", "lastName" ]
        }
      ]
    }
  }
}

Запрос внешних ссылок#

Внешние ссылки являются частным случаем объектов вложенного типа (embeddable), у которых есть примитивное поле entityId. В случае, если внешняя ссылка имеет тип, принадлежащий модели, то у нее есть также ссылочное поле entity и, в некоторых случаях, примитивное поле rootEntityId. Через ссылочное поле entity можно обращаться к сущности, на которую указывает ссылка, при условии, что она находится в той же базе.

В примере запрашивается Request, для которого запрашивается внешняя ссылка service, для которой в свою очередь запрашивается ссылочное поле entity (с примитивным полем code и beginDate).

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": "1",
  "params": {
    "request": {
      "type": "Request",
      "props": {
        "service": {
          "entity": {
            "props": [
              "code",
              "beginDate"
            ]
          }
        }
      }
    }
  }
}

Пример сложного запроса#

Предположим, что в модели имеются следующие сущности:

  • PerformedService — некоторый сервис (содержит ссылку на продукт и коллекцию операций);

  • ProductParty — продукт;

  • DepositCBExmpl — расширение ProductParty;

  • PerformedOperation — операции.

Необходимо найти все PerfromedService, которые ссылаются на DepositCBExmpl(ProductParty), а у данного DepositCBExmpl количество операций с датой окончания, отстоящей от даты начала более чем на 5 дней, больше или равно 2.

Условия:

  • У PerfromedService не должно быть операций, чей ExchangeRate превышает 10.

  • У PerfromedService должны совпадать поля Code и Name.

  • Дата открытия PerformedService должна быть в текущем году.

  • Код PerfromedService должен входить в заданный список.

Запрос делается постранично по 10 записей. При запросе первой страницы необходимо запросить общее количество элементов, удовлетворяющих условию отбора. При запросе последующих страниц общее количество элементов не запрашивать.

Для каждого найденного PerfromedService необходимо выбрать:

  • code;

  • name;

  • beginDate;

  • endDate;

  • product, который является DepositCBExmpl (является расширением ProductParty):

    • declaration (поле принадлежит DepositCBExmpl);

    • code (все следующие поля принадлежат ProductParty);

    • name;

    • beginDate;

    • nonCCI;

  • perfromedOperations, чей beginDate раньше недельной давности:

  • code;

  • name;

  • beginDate;

  • states, которые начинаются на "pref".

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

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": "1",
  "params": {
    "request": {
      "type": "PerformedService",
      "props": [
        "beginDate",
        "code",
        "endDate",
        "name",
        {
          "product": {
            "type": "DepositCBExmpl",
            "props": [
              "beginDate",
              "code",
              "name",
              "nonCCI",
              "declaration"
            ]
          },
          "performedOperations": {
            "type": "PerformedOperation",
            "props": [
              "beginDate",
              "code",
              "exchangeRate",
              "name",
              {
                "states": {
                  "cond": "elem$like'state%'"
                }
              }
            ],
            "cond": "elem.beginDate>D2020-07-07T13:24:58.736"
          }
        }
      ],
      "limit": 10,
      "cond": "root.product.performedOperations{cond=elem.endDate>(elem.beginDate+5)}.$count>=2&&root.performedOperations.exchangeRate.$max<10&&root.code==root.name&&root.beginDate>D2020-01-01T00:00:00.000&&root.code$in['bigTest_performedService','code2','code3']",
      "count": true
    }
  }
}

Запрос второй страницы выглядит идентичным образом за исключением раздела searchSpec. Добавился "offset" и исчез "count":

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": "1",
  "params": {
    "request": {
      "type": "PerformedService",
      "props": [
        "beginDate",
        "code",
        "endDate",
        "name",
        {
          "product": {
            "type": "DepositCBExmpl",
            "props": [
              "beginDate",
              "code",
              "name",
              "nonCCI",
              "declaration"
            ]
          },
          "performedOperations": {
            "type": "PerformedOperation",
            "props": [
              "beginDate",
              "code",
              "exchangeRate",
              "name",
              {
                "states": {
                  "cond": "elem$like'state%'"
                }
              }
            ],
            "cond": "elem.beginDate>D2020-07-07T13:24:58.736"
          }
        }
      ],
      "limit": 10,
      "offset": 10,
      "cond": "root.product.performedOperations{cond=elem.endDate>(elem.beginDate+5)}.$count>=2&&root.performedOperations.exchangeRate.$max<10&&root.code==root.name&&root.beginDate>D2020-01-01T00:00:00.000&&root.code$in['bigTest_performedService','code2','code3']"
    }
  }
}

Пример сложного ответа#

Ниже представлен вид ответа на запрос поиска:

{
  "elems": [
    {
      "type": "PerformedService",
      "id": "6849327559789903875",
      "props": {
        "beginDate": "2020-07-14T16:29:03.865",
        "code": "bigTest_performedService",
        "endDate": null,
        "name": "bigTest_performedService",
        "product": {
          "type": "DepositCBExmpl",
          "id": "6849327555494936577",
          "props": {
            "beginDate": "2020-07-14T16:29:03.865",
            "code": "bigTest_depositCBExmpl_code",
            "name": "bigTest_depositCBExmpl_name",
            "nonCCI": true,
            "declaration": "DepositCBExmpl declaration"
          }
        },
        "performedOperations": {
          "elems": [
            {
              "type": "PerformedOperation",
              "id": "6849327559789903876",
              "props": {
                "beginDate": "2020-07-14T16:29:03.865",
                "code": "bigTest_performedOperation3_code",
                "exchangeRate": "3.0000000000",
                "name": "bigTest_performedOperation3_name",
                "states": {
                  "elems": [
                    "state_1",
                    "state_2",
                    "state_3"
                  ]
                }
              }
            },
            {
              "type": "PerformedOperation",
              "id": "6849327559789903877",
              "props": {
                "beginDate": "2020-07-14T16:29:03.865",
                "code": "bigTest_performedOperation3_code",
                "exchangeRate": "6.0000000000",
                "name": "bigTest_performedOperation3_name",
                "states": {
                  "elems": []
                }
              }
            }
          ]
        }
      }
    }
  ],
  "count": 1
}

Контроллер мультипоиска#

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

Для включения контроллера необходимо на экземпляре dataspace–core задать следующие настройки:

  • dataspace.multisearch.enable=true — включение endpoint /multisearch;

  • dataspace.multisearch.endpoints=[{"name":"shard1","url":"http://127.0.0.1:8095/"},{"name":"shard2","url":"http://127.0.0.1:8096/"}] — список адресов шардов dataspace-core, на которые будут отправляться поисковые запросы.

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

  1. Если запрашивается только общее количество сущностей (значение поля count равно "true" и коллекция props пуста), то сортировка может быть не указана. Иначе — обязательно наличие хотя бы одного критерия сортировки.

  2. Обязательно задание поля limit, если запрашивается не только общее количество, а еще и поля. Если мультипоиск происходит по пользовательскому запросу, то наличие поля cond не обязательно, если по классу из модели — то обязательно задание cond.

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

Пример запроса мультипоиска:

{
  "jsonrpc": "2.0",
  "method": "execute",
  "id": "1",
  "params": {
    "request": {
      "type" : "Deposit",
      "props" : [ "code", "sum" ],
      "limit" : 2,
      "sort" : [ {
        "crit" : "root.lastCptDate"
      } ],
      "cond" : "root.code!=null",
      "count" : true,
      "context": {
        "shards": {
          "shard1": 1,
          "shard2": 1
        },
        "offset": 0,
        "limit": 2,
        "checksum": 23083794,
        "lastPageSize": 2
      }
    }
  }
}

Пример ответа на запрос мультипоиска:

{
  "jsonrpc" : "2.0",
  "id" : "1",
  "result" : {
    "elems" : [ {
      "type" : "Deposit",
      "id" : "1",
      "props" : {
        "code" : "Deposit 1",
        "sum" : "1.1"
      }
    }, {
      "type" : "Deposit",
      "id" : "2",
      "props" : {
        "code" : "Deposit 2",
        "sum" : "2.1"
      }
    } ],
    "count" : 34,
    "context" : {
      "shards" : {
        "shard2" : 1,
        "shard1" : 1
      },
      "offset" : 0,
      "limit" : 2,
      "checksum" : 1567590687,
      "lastPageSize" : 2
    }
  }
}

Предполагаемый путь работы с контекстом мультипоиска:

  1. Вызывается мультипоиск с limit = 20 и offset = 0, context не передается.

  2. Возвращается 20 элементов поиска и контекст.

  3. Для запроса следующей страницы (элементы с 21 по 40) вызывается тот же самый запрос, что и в п. 1, но с offset = 20. Рекомендуется передавать context из п. 2.

  4. Возвращаются следующие 20 элементов поиска и контекст.

  5. Для запроса элементов с 101 по 120 вызывается тот же запрос, что и в п. 3, но offset = 100. То есть происходит переход более чем на одну страницу. В данном случае контекст можно не передавать, так как он не повлияет на скорость поиска.

  6. Возвращается 20 элементов поиска и контекст.

  7. Вызывается предыдущая страница по отношению к запросу из п. 5, то есть offset выставляется равным "80". В данном случае переход происходит на страницу назад, поэтому контекст из п. 6 ускорит выполнение поиска.

Контроллер историцирования#

Контроллер историцирования предназначен для получения агрегированных историчных данных.

Примечание

Данные истории хранятся в не агрегированном виде, то есть записи хранят информацию только о новых значениях измененных полей. Неизмененные поля в записи истории имеют значения "null".

Контроллер историцирования предоставляет три API:

  • state — получение состояния сущности на заданный момент времени;

  • states — получение списка состояний сущности по состоянию на каждое изменение в рамках заданного интервала;

  • historical — получение списка изменений сущности за заданный интервал (вместо данной API рекомендуется использовать обычный поисковый запрос к сущности истории).

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

Пример запроса состояния сущности:

    {
      "jsonrpc": "2.0",
      "id": "1",
      "method": "state",
      "params": {
        "request": {
          "type": "ProductHistory",
          "props": [
            "code",
            "sysCodeCalculated",
            "name",
            "sysNameCalculated",
            "series",
            "sysSeriesCalculated",
            "comment",
            "sysCommentCalculated"
          ],
          "entityId": "7179975035859959809",
          "time": "2022-12-22T17:10:09.370896+03:00"
        }
      }
    }

где:

  • type — историцируемый тип (получается путем добавления постфикса "History" к имени историцируемого класса).

  • props — перечень запрашиваемых историцируемых полей.

    • <propertyName>Calculated — дополнительное системное виртуальное поле (для пользовательского историцируемого поля) — признак, что значение историцируемого поля удалось вычислить (в таблице истории есть данные для определения значения поля).

  • entityId — идентификатор объекта, по которому ищется история.

  • time — время, на которое рассчитывается состояние сущности.

Пример ответа на запрос состояния сущности:

    {
      "jsonrpc": "2.0",
      "id": "1",
      "result": {
        "type": "ProductHistory",
        "id": null,
        "props": {
          "code": "code1",
          "sysCodeCalculated": true,
          "name": "name2",
          "sysNameCalculated": true,
          "series": "series3",
          "sysSeriesCalculated": true,
          "comment": "someComment2",
          "sysCommentCalculated": true
        }
      }
    }

Получение списка состояний сущности#

Пример запроса списка состояний сущности:

    {
      "jsonrpc": "2.0",
      "id": "1",
      "method": "states",
      "params": {
        "request": {
          "type": "ProductHistory",
          "props": [
            "code",
            "sysCodeUpdated",
            "sysCommentCalculated",
            "name",
            "sysNameUpdated",
            "sysNameCalculated",
            "series",
            "sysSeriesUpdated",
            "sysSeriesCalculated",
            "comment",
            "sysCommentUpdated",
            "sysCodeCalculated",
            "sysState",
            "sysHistoryTime",
            "sysHistNumber"
          ],
          "entityId": "7179928514854584321",
          "timeFrom": "2022-12-12T14:09:37.3670109+03:00",
          "timeTo": "2023-01-01T14:09:37.3680069+03:00",
          "limit": 10,
          "offset": 0,
          "needCount": true,
          "sortDirection": "desc"
        }
      }
    }

где:

  • type — историцируемый тип (получается путем добавления постфикса "History" к имени историцируемого класса).

  • props — перечень запрашиваемых историцируемых полей.

    • <propertyName>Updated — дополнительное системное поле (для пользовательского историцируемого поля) — признак, что поле получило новое значение.

    • <propertyName>Calculated — дополнительное системное виртуальное поле (для пользовательского историцируемого поля) — признак, что значение историцируемого поля удалось вычислить (в таблице истории есть данные для определения значения поля).

    • sysState — вид изменения ("0" — создание; "1" — обновление; "2" — удаление; "3" — сохранение старого (предыдущего) состояния).

    • sysHistoryTime — время изменения сущности.

    • sysHistNumber — номер истории (каждая последующая запись имеет номер выше, чем предыдущая в рамках агрегата, но не обязательно последовательно).

  • entityId — идентификатор объекта, по которому ищется история.

  • timeFrom — время начала интервала, в пределах которого ищутся изменения (включающий интервал).

  • timeTo — время окончания интервала, в пределах которого ищутся изменения (включающий интервал).

  • limit — ограничение количества выбираемых записей.

  • offset — смещение от начала выбираемых записей.

  • needCount — запрос общего количества записей, удовлетворяющих условию запроса (без учета offset и limit).

  • sortDirection — направление сортировки ("asc" — в прямом хронологическом порядке; "desc" — в обратном хронологическом порядке).

Пример ответа на запрос списка состояний сущности (формат ответа на запрос историцирования аналогичен формату ответа на поисковый запрос):

    {
      "jsonrpc": "2.0",
      "id": "1",
      "result": {
        "elems": [
          {
            "type": "ProductHistory",
            "id": "7179928514854584324",
            "props": {
              "code": "code1",
              "sysCodeUpdated": null,
              "sysCodeCalculated": true,
              "name": "name2",
              "sysNameUpdated": null,
              "sysNameCalculated": true,
              "series": "series3",
              "sysSeriesUpdated": true,
              "sysSeriesCalculated": true,
              "comment": "someComment2",
              "sysCommentUpdated": null,
              "sysCommentCalculated": true,
              "sysState": "1",
              "sysHistoryTime": "2022-12-22T11:09:37.338Z",
              "sysHistNumber": "3"
            }
          },
          {
            "type": "ProductHistory",
            "id": "7179928514854584323",
            "props": {
              "code": "code1",
              "sysCodeUpdated": null,
              "sysCodeCalculated": true,
              "name": "name2",
              "sysNameUpdated": true,
              "sysNameCalculated": true,
              "series": "series1",
              "sysSeriesUpdated": null,
              "sysSeriesCalculated": true,
              "comment": "someComment2",
              "sysCommentUpdated": true,
              "sysCommentCalculated": true,
              "sysState": "1",
              "sysHistoryTime": "2022-12-22T11:09:37.322Z",
              "sysHistNumber": "2"
            }
          },
          {
            "type": "ProductHistory",
            "id": "7179928514854584322",
            "props": {
              "code": "code1",
              "sysCodeUpdated": null,
              "sysCodeCalculated": true,
              "name": "name1",
              "sysNameUpdated": null,
              "sysNameCalculated": true,
              "series": "series1",
              "sysSeriesUpdated": true,
              "sysSeriesCalculated": true,
              "comment": "someComment1",
              "sysCommentUpdated": null,
              "sysCommentCalculated": true,
              "sysState": "0",
              "sysHistoryTime": "2022-12-22T11:09:37.247Z",
              "sysHistNumber": "1"
            }
          }
        ],
        "count": 3
      }
    }

Получение списка изменений сущности за интервал#

Пример запроса списка изменений сущности за интервал:

    {
      "jsonrpc": "2.0",
      "id": "1",
      "method": "historical",
      "params": {
        "request": {
          "type": "ProductHistory",
          "props": [
            "code",
            "sysCodeUpdated",
            "name",
            "sysNameUpdated",
            "series",
            "sysSeriesUpdated",
            "comment",
            "sysCommentUpdated",
            "sysState",
            "sysHistoryTime",
            "sysHistNumber"
          ],
          "entityId": "7179982189413203969",
          "timeFrom": "2022-12-12T17:37:54.6505412+03:00",
          "timeTo": "2023-01-01T17:37:54.6515403+03:00",
          "limit": 10,
          "offset": 0,
          "needCount": true,
          "sortDirection": "desc"
        }
      }
    }

Атрибуты запроса такие же, как при запросе списка состояний сущности, за исключением невозможности использования <propertyName>Calculated поля.

Пример ответа на запрос списка изменений сущности за интервал:

    {
      "jsonrpc": "2.0",
      "id": "1",
      "result": {
        "elems": [
          {
            "type": "ProductHistory",
            "id": "7179982189413203972",
            "props": {
              "code": null,
              "sysCodeUpdated": null,
              "name": null,
              "sysNameUpdated": null,
              "series": "series3",
              "sysSeriesUpdated": true,
              "comment": null,
              "sysCommentUpdated": null,
              "sysState": "1",
              "sysHistoryTime": "2022-12-22T11:09:37.338Z",
              "sysHistNumber": "3"
            }
          },
          {
            "type": "ProductHistory",
            "id": "7179982189413203971",
            "props": {
              "code": null,
              "sysCodeUpdated": null,
              "name": "name2",
              "sysNameUpdated": true,
              "series": null,
              "sysSeriesUpdated": null,
              "comment": "someComment2",
              "sysCommentUpdated": true,
              "sysState": "1",
              "sysHistoryTime": "2022-12-22T11:09:37.322Z",
              "sysHistNumber": "2"
            }
          },
          {
            "type": "ProductHistory",
            "id": "7179982189413203970",
            "props": {
              "code": "code1",
              "sysCodeUpdated": true,
              "name": "name1",
              "sysNameUpdated": true,
              "series": "series1",
              "sysSeriesUpdated": true,
              "comment": "someComment1",
              "sysCommentUpdated": true,
              "sysState": "0",
              "sysHistoryTime": "2022-12-22T11:09:37.247Z",
              "sysHistNumber": "1"
            }
          }
        ],
        "count": 3
      }
    }