Протокол GraphQL компонента DataSpace Core#

Введение#

Компонент DataSpace Core продукта DataSpace предоставляет потребителю возможность вызывать основные функции по протоколу GraphQL. При этом схема GraphQL строится на основе модели данных предметной области потребителя.

Настройка#

Взаимодействие с серверной частью DataSpace осуществляется через точку доступа со следующим URL-адресом: {серверURL}/graphql.

Параметр, включающий endpoint GraphQL — dataspace.endpoint.graphql.enabled=true.

Параметр, включающий браузерный редактор GraphQL — dataspace.endpoint.graphiql.enabled=true.

Для настройки работы endpoint /graphiql через ingress необходимо так же определить параметры

graphiql.endpoint.graphql=/<path>/graphql
graphiql.basePath=/<path>
graphiql.endpoint.subscriptions=/<path>/subscriptions

где <path> - путь к сервису, согласно правилам на Ingress. Это важно для выполнения запросов с формы /graphiql

Для изменения endpoint можно задать параметр — graphql.url=....

Элементы схемы#

Примитивные типы#

Примитивные типы модели отображаются в следующие скалярные типы схемы:

Примитивный тип модели

Скалярный тип схемы

Формат

Пример значения

Character

Char

"a"

String

String

"Hello!"

Text

String

"Text!"

Byte

Byte

123

Short

Short

12345

Integer

Int

1234567890

Long

Long

1234567890123456789

Float

_Float4

1234.567

Double

Float

1234567890.012345

BigDecimal

BigDecimal

1234567890123456789.0123456789

Date

_DateTime

ISO 8601

"2020-02-22T11:49:10.123"

LocalDate

_Date

ISO 8601

"2020-02-22"

LocalDateTime

_DateTime

ISO 8601

"2020-02-22T11:49:10.123"

OffsetDateTime

_OffsetDateTime

ISO 8601

"2020-02-22T08:49:10.123Z"

Boolean

Boolean

true

byte[]

_ByteArray

Base64

"SGVsbG8h"

В случае, если в модели используются коллекции примитивов, то для них дополнительно инициализируются типы объектов _${наименование скалярного типа без префикса '_'}Collection со следующими полями:

  • elems: [${наименование скалярного типа}!]!: элементы коллекции;

  • count: Int!: количество элементов в коллекции.

Пример: коллекция символов и коллекция дат#

type _CharCollection {
    elems: [Char!]!
    count: Int!
}

type _DateCollection {
    elems: [_Date!]!
    count: Int!
}

Технические элементы#

Для поддержки работы основных функций на схеме введены различные технические элементы:

  • директива mergeReqSpec (спецификация запроса для слияния) для встроенных фрагментов с аргументом cond: String!: условие поиска в грамматике строковых выражений;

  • интерфейс _Entity (сущность) с полем id: ID!: идентификатор сущности;

  • перечисление _SortOrder (порядок сортировки) с двумя допустимыми значениями ASC (по возрастанию) и DESC (по убыванию);

  • входной тип _SortCriterionSpecification (спецификация критерия сортировки) с полями:

    • crit: String!: критерий сортировки в грамматике строковых выражений;

    • order: _SortOrder! = ASC: порядок сортировки;

    • nullsLast: Boolean: признак следования null-значений в конце;

  • тип объекта _MergedEntitiesCollection (коллекция слитых сущностей) с полями:

    • elems: [_Entity!]!: элементы коллекции;

    • count: Int!: количество элементов в коллекции;

  • входной тип _SingleReferenceInput (входные данные внешней ссылки на агрегат) с полем entityId: String!: идентификатор сущности;

  • входной тип _SingleReferenceSetInput (входные данные коллекции внешних ссылок на агрегаты) с полями:

    • add: [_SingleReferenceInput]: список ссылок для добавления;

    • remove: [_SingleReferenceInput]: список ссылок для удаления;

    • clear: Boolean: признак очистки коллекции;

  • входной тип _DoubleReferenceInput (входные данные внешней ссылки на сущность) с полями:

    • entityId: String!: идентификатор сущности;

    • rootEntityId: String!: идентификатор агрегата;

  • входной тип _DoubleReferenceSetInput (входные данные коллекции внешних ссылок на сущности) с полями:

    • add: [_DoubleReferenceInput]: список ссылок для добавления;

    • remove: [_DoubleReferenceInput]: список ссылок для удаления;

    • clear: Boolean: признак очистки коллекции;

  • входной тип _StatusInput (входные данные статуса) с полями:

    • code: String!: код статуса;

    • reason: String: причина изменения статуса;

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

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

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

    • timeout: Long!: таймаут блокировки:

    • reason: String: причина блокировки;

  • входной тип _UnlockInput (входные данные снятия прикладной блокировки) с полями:

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

    • token: String!: токен блокировки;

  • тип объекта LockOutput (вывод блокировки) с полями:

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

    • result: Boolean: результат (успех блокировки);

    • failReason: String: причина неудачи;

    • timeoutEndTime: Long: время окончания таймаута;

  • Интерефейс _Reference (ссылка) с полем entityId: String: идентификатор сущности.

Описания элементов на схеме:

interface _Entity {
    id: ID!
}

enum _SortOrder {
    ASC
    DESC
}

input _SortCriterionSpecification {
    crit: String!
    order: _SortOrder! = ASC
    nullsLast: Boolean
}

type _MergedEntitiesCollection {
    elems: [_Entity!]!
    count: Int!
}

input _SingleReferenceInput {
    entityId: String!
}

input _SingleReferenceSetInput {
    add: [_SingleReferenceInput]
    remove: [_SingleReferenceInput]
    clear: Boolean
}

input _DoubleReferenceInput {
    entityId: String!
    rootEntityId: String!
}

input _DoubleReferenceSetInput {
    add: [_DoubleReferenceInput]
    remove: [_DoubleReferenceInput]
    clear: Boolean
}

input _StatusInput {
    code: String!
    reason: String
}

input _TryLockInput {
    id: ID!
    token: String
    timeout: Long!
    reason: String
}

input _UnlockInput {
    id: ID!
    token: String!
}

type LockOutput {
    token: String
    result: Boolean
    failReason: String
    timeoutEndTime: Long
}

interface _Reference {
    entityId: String
}

Типы перечисления#

Типы перечислений модели отображаются в типы перечислений схемы _EN_${наименование типа перечисления модели} с переносом допустимых значений без изменений.

Пример: атрибут#

enum _EN_Attribute {
    SYSTEM
    READ_ONLY
    HIDDEN
}

В случае, если в модели используются коллекции перечислений, то для них дополнительно инициализируются типы объектов _ENC_${наименование типа перечисления модели} со следующими полями:

  • elems: [${тип объекта перечисления}!]!: элементы коллекции;

  • count: Int!: количество элементов в коллекции.

Пример: коллекция атрибутов#

type _ENC_Attribute {
    elems: [_EN_Attribute!]!
    count: Int!
}

Классы модели#

В протоколе GraphQL нет поддержки наследования типов объектов. Поэтому для поддержки наследования классов в модели на схеме GraphQL для каждого класса модели выполняется следующее:

  • Создается интерфейс ${наименование класса модели} с полями:

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

    • aggVersion: Long!: версия агрегата сущности;

    • поля для вычислимых свойств, имеющих вид _get${наименование скалярного типа}(expression: String!): ${наименование скалярного типа}, где expression — выражение в терминах грамматики строковых выражений;

    • поля для каждого свойства класса модели, включая свойства родительского класса.

  • Создается тип объекта _E_${наименование класса модели}, реализующий: * интерфейс _Entity; * интерфейс соответствующий классу модели; * интерфейсы, соответствующие всем родительским классам модели. Данный тип объекта с полями: * id: ID! — идентификатор сущности; * aggVersion: Long! — версия агрегата сущности; * поля для вычислимых свойств, имеющих вид _get${наименование скалярного типа}(expression: String!): ${наименование скалярного типа}, где expression — выражение в терминах грамматики строковых выражений; * поля для каждого свойства класса модели включая свойства родительского класса.

  • Создается тип для коллекции сущностей _EC_${наименование класса модели} с полями:

    • elems: [${интерфейс класса модели}!]! — элементы коллекции;

    • count: Int! — количество элементов в коллекции.

  • Если имеются внешние ссылки на данный класс модели, то создается тип для внешней ссылки _G_${наименование класса модели}Reference с полями:

    • entityId: String — идентификатор сущности;

    • rootEntityId: String — идентификатор агрегата сущности (есть только в случае, если класс модели не является агрегатом);

    • entity: ${наименование класса модели} — ссылка на сущность для запроса данных в текущем шарде.

  • Создается тип для набора свойств сущности _SE_${наименование класса модели} с полями:

    • id: ID! — идентификатор сущности;

    • aggVersion: Long! — версия агрегата сущности;

    • поля для вычислимых свойств, имеющих вид _get${наименование скалярного типа}(expression: String!): ${наименование скалярного типа}, где expression — выражение в терминах грамматики строковых выражений;

    • поля для примитивных свойства класса модели включая свойства родительского класса.

  • Создается тип коллекции наборов свойств _SEC_${наименование класса модели} со следующими полями:

    • elems: [_SE_${наименование класса модели}!]! — элементы коллекции;

    • count: Int! — количество элементов в коллекции.

  • Создается тип для ссылки на сущность _R_${наименование класса модели}:

    • реализующий интерфейс _Reference;

    • с полями:

      • entityId: String - идентификатор сущности;

      • entity: ${интерфейс класса модели} - сущность.

  • Создается тип для результа мультипоиска _ECM_${наименование класса модели} с полями:

    • elems: [${интерфейс класса модели}!]! — элементы коллекции;

    • count: Int! — количество элементов в коллекции;

    • ctx: String - контекст мультипоиска, сериализованный в строку. Пример структуры контекста можно посмотреть здесь

В зависимости от типа свойства, соответствующее поле имеет определенный тип и аргументы:

  • примитив/перечисление — соответствующий скалярный тип/тип перечисления и не имеет аргументов;

  • коллекция примитивов/перечислений — соответствующий примитиву/перечислению тип объекта коллекции и аргументы:

    • cond: String — условие фильтрации в грамматике строковых выражений;

    • limit: Int — ограничение на количество элементов;

    • offset: Int — смещение;

    • sort: [_SortCriterionSpecification!] — сортировка;

  • ссылка — соответствующий классу модели интерфейс и аргумент alias: String — псевдоним;

  • коллекция ссылок — соответствующий классу модели тип объекта коллекции и аргументы:

    • elemAlias: String — псевдоним элемента;

    • cond: String — условие фильтрации в грамматике строковых выражений;

    • limit: Int — ограничение на количество элементов;

    • offset: Int — смещение;

    • sort: [_SortCriterionSpecification!] — сортировка.

Данные типы преимущественно используются для запроса данных у сервиса.

Помимо этого для поддержки мутаций:

  • Создается входной тип для создания сущности _Create${наименование класса модели}Input с полями:

    • поля для каждого свойства класса модели, включая свойства родительского класса (за исключением свойств, являющихся mappedBy-ссылками/коллекциями ссылок);

    • поля для каждого наблюдателя статуса класса модели statusFor${наименование наблюдателя}: _StatusInput.

  • Создается входной тип для обновления сущности _Update${наименование класса модели}Input с полями:

    • id: ID! — идентификатор сущности;

    • поля для каждого свойства класса модели, включая свойства родительского класса за исключением:

      • свойств, являющихся mappedBy-ссылками/коллекциями ссылок;

      • свойств, имеющих признак parent=true.

  • Для типов сущностей, поддерживающих compare, создается входной тип _Compate${наименование класса модели}Input с полями участниками compare.

  • Для типов сущностей, поддерживающих inc, создается входной тип _Inc${наименование класса модели}Input с полями участниками inc. Тип поля зависит от типа свойства модели. Возможны следующие типы:

    • _IncIntValueInput: для типа свойства Integer;

    • _IncLongValueInput: для типа свойства Long;

    • _IncFloatValueInput: для типа свойства Float;

    • _IncDoubleValueInput: для типа свойства Double;

    • _IncBigDecimalValueInput: для типа свойства BigDecimal.

  • Типы _Inc${тип поля inc}ValueInput включат два атрибута:

    • value: обязательный атрибут значения для суммирования;

    • fail: опциональный атрибут условия проверки вычисленного значения поля. Тип значения _Inc${тип поля inc}ValueFailInput содержит обязательные атрибуты:

      • operator: логический оператор сравнения _IncFailOperator:

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

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

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

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

      • value: значение, с которым сравнивается вычисленное значение.

    • Для типов сущностей, поддерживающих updateOrCreate, создаются:

      • входной тип _Exist${наименование класса модели}Input;

      • входной тип _ExistUpdate${наименование класса модели}Input;

      • перечисление с именами уникальных индексов _Key${наименование класса модели определяющего уникальные индексы};

      • тип _UpdateOrCreate${наименование класса модели}Response, включающий:

        • атрибут created: Boolean для указания на создание true сущности командой;

        • атрибут returning: ${наименование класса модели} для чтения атрибутов сущности по аналогии с create, update', get.

    • Для результата команды updateOrCreateMany определяется тип _UpdateOrCreateManyResponse, включающий:

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

      • атрибут created: Boolean для указания на создание true сущности командой.

    • Для типов сущностей, поддерживающий compare и/или inc, создается тип UpdateMany${наименование класса модели}Input.

    • Для типов сущностей, поддерживающий compare и/или inc, создается тип DeleteMany${наименование класса модели}Input.

В зависимости от типа свойства соответствующее поле имеет определенный тип:

  • примитив/перечисление — соответствующий скалярный тип/тип перечисления;

  • коллекция примитивов/перечислений — [${наименование соответствующего скалярного типа/типа перечисления}];

  • ссылка — ID;

  • внешняя ссылка на агрегат — _SingleReferenceInput;

  • коллекция внешних ссылок на агрегаты — [_SingleReferenceSetInput];

  • внешняя ссылка на сущность — _DoubleReferenceInput;

  • коллекция внешних ссылок на агрегаты — [_DoubleReferenceSetInput].

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

interface Product {
    id: ID!
    aggVersion: Long!
    _getChar(expression: String!): Char
    _getString(expression: String!): String
    _getByte(expression: String!): Byte
    _getShort(expression: String!): Short
    _getInt(expression: String!): Int
    _getLong(expression: String!): Long
    _getFloat(expression: String!): _Float4
    _getDouble(expression: String!): Float
    _getBigDecimal(expression: String!): BigDecimal
    _getDate(expression: String!): _Date
    _getDateTime(expression: String!): _DateTime
    _getOffsetDateTime(expression: String!): _OffsetDateTime
    _getBoolean(expression: String!): Boolean
    _getByteArray(expression: String!): _ByteArray
    type: String!
    lastChangeDate: _DateTime
    chgCnt: Long
    code: String!
    states(cond: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _StringCollection!
    document(alias: String): Document
    services(cond: String, elemAlias: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _EC_Service!
    contract: _G_ContractReference!
    relatedProducts(cond: String, elemAlias: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _EC_ProductProductElementReference!
    statusForPlatform(alias: String): Status
    statusForService(alias: String): Status
}

type _E_Product implements Product & _Entity {
    id: ID!
    aggVersion: Long!
    _getChar(expression: String!): Char
    _getString(expression: String!): String
    _getByte(expression: String!): Byte
    _getShort(expression: String!): Short
    _getInt(expression: String!): Int
    _getLong(expression: String!): Long
    _getFloat(expression: String!): _Float4
    _getDouble(expression: String!): Float
    _getBigDecimal(expression: String!): BigDecimal
    _getDate(expression: String!): _Date
    _getDateTime(expression: String!): _DateTime
    _getOffsetDateTime(expression: String!): _OffsetDateTime
    _getBoolean(expression: String!): Boolean
    _getByteArray(expression: String!): _ByteArray
    type: String!
    lastChangeDate: _DateTime
    chgCnt: Long
    code: String!
    states(cond: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _StringCollection!
    document(alias: String): Document
    services(cond: String, elemAlias: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _EC_Service!
    contract: _G_ContractReference!
    relatedProducts(cond: String, elemAlias: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _EC_ProductProductElementReference!
    statusForPlatform(alias: String): Status
    statusForService(alias: String): Status
}

type _EC_Product {
    elems: [Product!]!
    count: Int!
}

type _G_ProductReference {
    entityId: String
    entity: Product
}

type _SE_Product {
    id: ID!
    aggVersion: Long!
    _getChar(expression: String!): Char
    _getString(expression: String!): String
    _getByte(expression: String!): Byte
    _getShort(expression: String!): Short
    _getInt(expression: String!): Int
    _getLong(expression: String!): Long
    _getFloat(expression: String!): _Float4
    _getDouble(expression: String!): Float
    _getBigDecimal(expression: String!): BigDecimal
    _getDate(expression: String!): _Date
    _getDateTime(expression: String!): _DateTime
    _getOffsetDateTime(expression: String!): _OffsetDateTime
    _getBoolean(expression: String!): Boolean
    _getByteArray(expression: String!): _ByteArray
    type: String!
    lastChangeDate: _DateTime
    chgCnt: Long
    code: String!
}

type _SEC_Product {
    elems: [_SE_Product!]!
    count: Int!
}

type _R_Product implements _Reference {
    entityId: String
    entity: Product
}

input _CreateProductInput {
    code: String!
    states: [String]
    contract: _SingleReferenceInput
    relatedProducts: _SingleReferenceSetInput
    statusForPlatform: _StatusInput
    statusForService: _StatusInput
}

input _UpdateProductInput {
    id: ID!
    code: String
    states: [String]
    contract: _SingleReferenceInput
    relatedProducts: _SingleReferenceSetInput
    statusForPlatform: _StatusInput
    statusForService: _StatusInput
}

interface Deposit {
    id: ID!
    aggVersion: Long!
    _getChar(expression: String!): Char
    _getString(expression: String!): String
    _getByte(expression: String!): Byte
    _getShort(expression: String!): Short
    _getInt(expression: String!): Int
    _getLong(expression: String!): Long
    _getFloat(expression: String!): _Float4
    _getDouble(expression: String!): Float
    _getBigDecimal(expression: String!): BigDecimal
    _getDate(expression: String!): _Date
    _getDateTime(expression: String!): _DateTime
    _getOffsetDateTime(expression: String!): _OffsetDateTime
    _getBoolean(expression: String!): Boolean
    _getByteArray(expression: String!): _ByteArray
    type: String!
    lastChangeDate: _DateTime
    chgCnt: Long
    code: String!
    rate: BigDecimal
    states(cond: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _StringCollection!
    document(alias: String): Document
    services(cond: String, elemAlias: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _EC_Service!
    contract: _G_ContractReference!
    relatedProducts(cond: String, elemAlias: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _EC_ProductProductElementReference!
    statusForPlatform(alias: String): Status
    statusForService(alias: String): Status
}

type _E_Deposit implements Deposit & Product & _Entity {
    id: ID!
    aggVersion: Long!
    _getChar(expression: String!): Char
    _getString(expression: String!): String
    _getByte(expression: String!): Byte
    _getShort(expression: String!): Short
    _getInt(expression: String!): Int
    _getLong(expression: String!): Long
    _getFloat(expression: String!): _Float4
    _getDouble(expression: String!): Float
    _getBigDecimal(expression: String!): BigDecimal
    _getDate(expression: String!): _Date
    _getDateTime(expression: String!): _DateTime
    _getOffsetDateTime(expression: String!): _OffsetDateTime
    _getBoolean(expression: String!): Boolean
    _getByteArray(expression: String!): _ByteArray
    type: String!
    lastChangeDate: _DateTime
    chgCnt: Long
    code: String!
    rate: BigDecimal
    states(cond: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _StringCollection!
    document(alias: String): Document
    services(cond: String, elemAlias: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _EC_Service!
    contract: _G_ContractReference!
    relatedProducts(cond: String, elemAlias: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _EC_ProductProductElementReference!
    statusForPlatform(alias: String): Status
    statusForService(alias: String): Status
}

type _EC_Deposit {
    elems: [Deposit!]!
    count: Int!
}

type _SE_Deposit {
    id: ID!
    aggVersion: Long!
    _getChar(expression: String!): Char
    _getString(expression: String!): String
    _getByte(expression: String!): Byte
    _getShort(expression: String!): Short
    _getInt(expression: String!): Int
    _getLong(expression: String!): Long
    _getFloat(expression: String!): _Float4
    _getDouble(expression: String!): Float
    _getBigDecimal(expression: String!): BigDecimal
    _getDate(expression: String!): _Date
    _getDateTime(expression: String!): _DateTime
    _getOffsetDateTime(expression: String!): _OffsetDateTime
    _getBoolean(expression: String!): Boolean
    _getByteArray(expression: String!): _ByteArray
    type: String!
    lastChangeDate: _DateTime
    chgCnt: Long
    code: String!
    rate: BigDecimal
}

type _SEC_Deposit {
    elems: [_SE_Deposit!]!
    count: Int!
}

type _R_Deposit implements _Reference {
    entityId: String
    entity: Deposit
}

input _CreateDepositInput {
    code: String!
    rate: BigDecimal
    states: [String]
    contract: _SingleReferenceInput
    relatedProducts: _SingleReferenceSetInput
    statusForPlatform: _StatusInput
    statusForService: _StatusInput
}

input _UpdateDepositInput {
    id: ID!
    code: String
    rate: BigDecimal
    states: [String]
    contract: _SingleReferenceInput
    relatedProducts: _SingleReferenceSetInput
    statusForPlatform: _StatusInput
    statusForService: _StatusInput
}

Статусы сущностей#

Если для какой-либо сущности описаны ее наблюдатели, статусы и переходы между ними, то в модели неявно создаются:

  • класс Stakeholder (наблюдатель) со свойствами:

    • code: String — код наблюдателя;

    • name: String — наименование наблюдателя;

  • класс Status (статус) со свойствами:

    • code: String — код статуса;

    • name: String — наименование статуса;

    • description: String — описание статуса;

    • statusType: String — тип статуса;

    • initial: Boolean — признак является ли статус начальным;

    • stakeholder: Stateholder — ссылка на наблюдателя;

  • класс StatusGraph (переход между статусами) со свойствами:

    • code: String — код перехода;

    • name: String — наименование перехода;

    • statusFrom: Status — статус, из которого происходит переход;

    • statusTo: Status — статус, в которой происходит переход.

Для данных классов генерируются те же интерфейсы и типы объектов, как и для обычных классов модели, за исключением:

  • типа объекта для внешней ссылки;

  • входных типов для создания/обновления.

Их описания на схеме:

interface Stakeholder {
    id: ID!
    aggVersion: Long!
    _getChar(expression: String!): Char
    _getString(expression: String!): String
    _getByte(expression: String!): Byte
    _getShort(expression: String!): Short
    _getInt(expression: String!): Int
    _getLong(expression: String!): Long
    _getFloat(expression: String!): _Float4
    _getDouble(expression: String!): Float
    _getBigDecimal(expression: String!): BigDecimal
    _getDate(expression: String!): _Date
    _getDateTime(expression: String!): _DateTime
    _getOffsetDateTime(expression: String!): _OffsetDateTime
    _getBoolean(expression: String!): Boolean
    _getByteArray(expression: String!): _ByteArray
    lastChangeDate: _DateTime
    chgCnt: Long
    code: String
    name: String
}

type _E_Stakeholder implements _Entity & Stakeholder {
    id: ID!
    aggVersion: Long!
    _getChar(expression: String!): Char
    _getString(expression: String!): String
    _getByte(expression: String!): Byte
    _getShort(expression: String!): Short
    _getInt(expression: String!): Int
    _getLong(expression: String!): Long
    _getFloat(expression: String!): _Float4
    _getDouble(expression: String!): Float
    _getBigDecimal(expression: String!): BigDecimal
    _getDate(expression: String!): _Date
    _getDateTime(expression: String!): _DateTime
    _getOffsetDateTime(expression: String!): _OffsetDateTime
    _getBoolean(expression: String!): Boolean
    _getByteArray(expression: String!): _ByteArray
    lastChangeDate: _DateTime
    chgCnt: Long
    code: String
    name: String
}

type _EC_Stakeholder {
    elems: [Stakeholder!]!
    count: Int!
}

interface Status {
    id: ID!
    aggVersion: Long!
    _getChar(expression: String!): Char
    _getString(expression: String!): String
    _getByte(expression: String!): Byte
    _getShort(expression: String!): Short
    _getInt(expression: String!): Int
    _getLong(expression: String!): Long
    _getFloat(expression: String!): _Float4
    _getDouble(expression: String!): Float
    _getBigDecimal(expression: String!): BigDecimal
    _getDate(expression: String!): _Date
    _getDateTime(expression: String!): _DateTime
    _getOffsetDateTime(expression: String!): _OffsetDateTime
    _getBoolean(expression: String!): Boolean
    _getByteArray(expression: String!): _ByteArray
    lastChangeDate: _DateTime
    chgCnt: Long
    code: String
    name: String
    description: String
    statusType: String
    initial: Boolean
    stakeholder(alias: String): Stakeholder
}

type _E_Status implements _Entity & Status {
    id: ID!
    aggVersion: Long!
    _getChar(expression: String!): Char
    _getString(expression: String!): String
    _getByte(expression: String!): Byte
    _getShort(expression: String!): Short
    _getInt(expression: String!): Int
    _getLong(expression: String!): Long
    _getFloat(expression: String!): _Float4
    _getDouble(expression: String!): Float
    _getBigDecimal(expression: String!): BigDecimal
    _getDate(expression: String!): _Date
    _getDateTime(expression: String!): _DateTime
    _getOffsetDateTime(expression: String!): _OffsetDateTime
    _getBoolean(expression: String!): Boolean
    _getByteArray(expression: String!): _ByteArray
    lastChangeDate: _DateTime
    chgCnt: Long
    code: String
    name: String
    description: String
    statusType: String
    initial: Boolean
    stakeholder(alias: String): Stakeholder
}

type _EC_Status {
    elems: [Status!]!
    count: Int!
}

Тип объекта _Packet#

Специализированный тип объекта _Packet используется для описания пакета.

Поля пакета делятся на две группы. К первой группе относятся поля, описывающие результат выполнения уровня пакета:

  • aggregateVersion: Long содержит версию агрегата, для которого выполнен пакет;

  • isIdempotenceResponse: Boolean содержит признак идемпотентного вызова пакета.

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

  • create${наименование класса модели}(input: _Create${наименование класса модели}Input!): ${наименование класса модели} — определяет команду создания сущности.

  • get${наименование класса модели}(id: ID!): ${наименование класса модели} — определяет команду чтения сущности.

  • update${наименование класса модели}(input: _Update${наименование класса модели}Input!, compare: _Compare${наименование класса модели}, inc: _Inc${наименование класса модели}): ${наименование класса модели} — определяет команду изменения сущности. Опциональный параметр compare определяет значения для свойств сущности на соответствие ожидаемым. Опциональный параметр inc определяет значения инкремента свойств сущности.

  • updateOrCreate${наименование класса модели}(input: _Create${наименование класса модели}Input!, exist: _Exist${наименование класса модели}): _UpdateOrCreate${наименование класса модели}Response — выполняет поиск сущности по критерию exist. Если сущность не создана, то создает с параметрами input, иначе применяет изменение по параметрам input или значениями объекта update параметра exist при их заполнении.

  • delete${наименование класса модели}(id: ID!, compare: _Compare${наименование класса модели}): String — определяет команду удаления сущности. Опциональный параметр compare определяет значения для свойств сущности на соответствие ожидаемым.

Для класса модели с указанной возможностью применения прикладной блокировки и всех его наследников дополнительно формируются две операции:

  • tryLock${наименование класса модели}(input: _TryLockInput!): LockOutput — определяет команду блокировки сущности;

  • unlock${наименование класса модели}(input: _UnlockInput!): LockOutput — определяет команду разблокировки сущности.

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

Модуль выполняет проверку переданных в пакете команд значений типа BigDecimal на соответствие заданных в модели length и scale. Детальное изложение в разделе документации Протокол JSON-RPC компонента DataSpace Core.

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

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

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

mutation {
  
  packet {

    createSampleEntity(input: {
      code: "sample code"
      name: "sample name"
    }) {
      id
    }
    
    updateSampleEntity(
      input: { 
        id: "ref:createSampleEntity"
        code: "new sample code"
        name: "new sample name"
      }
      compare: {
        code: "sample code"
        name: "wrong sample name"
      }
    ) {
      code
      name
    }
    
  }
  
}

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

{
  "errors": [
    {
      "message": "Ошибка обработки команды id = 'updateSampleEntity', name = 'update': Ошибка обработки сравниваемого поля 'name': Расхождение ожидаемого 'wrong sample name' и фактического 'sample name' значений",
      "locations": [
        {
          "line": 3,
          "column": 3
        }
      ],
      "extensions": {
        "classification": "COMPARE_NOT_EQUAL"
      }
    }
  ],
  "data": {
    "packet": null
  }
}

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

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

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

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

mutation {
  
  packet {
    
    createSampleEntity(
      input:{ counter: 9, sum: 3.14 }
    ) { 
      id 
      counter 
      sum 
    }
    
    updateSampleEntity(
      input: { id: "ref:createSampleEntity" }
      inc: {
        counter: { value: -4 }
        sum: { value: 42 }
      }
    ) { 
      counter
      sum
    }
    
  }
  
}

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

{
  "data": {
    "packet": {
      "createSampleEntity": {
        "id": "7142520608695844865",
        "counter": 9,
        "sum": 3.14
      },
      "updateSampleEntity": {
        "counter": 5,
        "sum": 45.14
      }
    }
  }
}

Параметр inc выполняет операцию сложения текущего значения поля в базе на указанную величину. В примере поле sum увеличено на 42, а поле counter уменьшено на 4. Выполнение команды создания необходимо для полноты примера.

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

Пример:

mutation {
  
  packet {
    
    createSampleEntity(
      input:{ sum: 3.14 }
    ) { 
      id 
      sum 
    }
    
    updateSampleEntity(
      input: { id: "ref:createSampleEntity" }
      inc: {
        sum: { 
          value: -5
          fail: {
            operator: lt
            value: 0
          }
        }
      }
    ) { 
      sum
    }
    
  }
  
}

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

{
  "errors": [
    {
      "message": "Ошибка обработки команды id = 'updateSampleEntity', name = 'update': Ошибка обработки инкриминируемого поля 'sum': Новое значение поля '-1.86', полученное после сложения с '-5', нарушает ограничение 'LESS 0'",
      "locations": [
        {
          "line": 3,
          "column": 3
        }
      ],
      "extensions": {
        "classification": "INC_FAIL_EXCEPTION"
      }
    }
  ],
  "data": {
    "packet": null
  }
}

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

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

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

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

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

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

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

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

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

Примечание

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

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

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

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

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

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

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

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

Схемы GraphQL для этой модели — во вложенном файле graphql-schema-sample-min.

Пример мутации:

mutation {
  packet {
    updateOrCreateSample(
      input: {id: "42", code: "1", name: "1"}
      exist: { update: { name: "2" } }
    ) 
    {
      created
      returning {
        code
        name
      }
    }
  }
}

Первый вызов мутации вернет результат:

{
  "data": {
    "packet": {
      "updateOrCreateSample": {
        "created": true,
        "returning": {
          "code": "1",
          "name": "1"
        }
      }
    }
  }
}

Второй вызов даст результат:

{
  "data": {
    "packet": {
      "updateOrCreateSample": {
        "created": false,
        "returning": {
          "code": "1",
          "name": "2"
        }
      }
    }
  }
}

При повторном вызове для существующей сущности с идентификатором 42 выполнено частичное изменение, а именно — изменение поля name. Остальные поля сущности не изменились.

Использование команды get#

Для каждого типа сущности модели в пакете формируется выделенная команда чтения get, позволяющая получить сущность по идентификатору, указанному в аргументе команды id.

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

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

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

mutation {
  
  packet {
    
    createSample(input: { code: "sample code" }) { id }
    
    getById: getSample(id: "ref:createSample") { id code }
    
    getByCode: getSample(id:"find:root.code=='sample code'") { id code }
    
    emptyGetByCode: getSample(id:"find:root.code=='unknown sample code'") { id code }
    
  }
  
}

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

{
  "data": {
    "packet": {
      "createSample": {
        "id": "7139533211695644673"
      },
      "getById": {
        "id": "7139533211695644673",
        "code": "sample code"
      },
      "getByCode": {
        "id": "7139533211695644673",
        "code": "sample code"
      },
      "emptyGetByCode": null
    }
  }
}

В примере последняя команда get возвращает пустой результат, так как сущности с code равным unknown sample code не существует.

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

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

mutation {

    packet {

        getSample(id:"42", lock: WAIT) { id code }

    }

}

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

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

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

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

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

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

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

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

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

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

mutation{
  packet{
    getSample(id:"unknown-entity", failOnEmpty: false) { code }
  }
}

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

{
  "data": {
    "packet": {
      "getSample": null
    }
  }
}

Внимание!

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

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

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

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

enum _DependsOnDependencyByGet {
    EXISTS
    NOT_EXISTS
}

enum _DependsOnDependencyByUpdateOrCreate {
    CREATED
    NOT_CREATED
}

directive @dependsOnByGet(commandId: String!, dependency: _DependsOnDependencyByGet!) repeatable on FIELD

directive @dependsOnByUpdateOrCreate(commandId: String!, dependency: _DependsOnDependencyByUpdateOrCreate!) repeatable on FIELD
  • commandId: строковый идентификатор команды источника результата;

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

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

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

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

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

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

Директивы могут быть применены к любым командам, кроме команды get.

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

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

mutation{
  packet{    
    c: updateOrCreateSample(input:{id:"42"}) 
    	{ created }
    
    createSample(input:{id:"SUB-42" code: "initial code"}) 
    	@dependsOnByUpdateOrCreate(commandId:"c", dependency: CREATED)
    	{ id code }
    
    g: getSample(id:"SUB-42") { id code }
    updateSample(input:{id:"SUB-42" code: "updated code"})
      @dependsOnByUpdateOrCreate(commandId:"c", dependency: NOT_CREATED)
    	@dependsOnByGet(commandId:"g", dependency: EXISTS)
    	{ code }
  }
}

Предполагается, что отсутствуют объекты Sample с id, равным 42. Первое выполнение пакета даст результат:

{
  "data": {
    "packet": {
      "c": {
        "created": true
      },
      "createSample": {
        "id": "SUB-42",
        "code": "initial code"
      },
      "g": {
        "id": "SUB-42",
        "code": "initial code"
      },
      "updateSample": null
    }
  }
}

Пояснение: вторая команда пакета createSample должна быть выполнена только в том случае, если первая команда updateOrCreate создала сущность. Четвертая команда updateSample должна быть выполнена, если первая команда updateOrCreate не создала сущность, и третья команда getSample вернула не пустой результат.

Второй вызов пакета имеет следующий результат:

{
  "data": {
    "packet": {
      "c": {
        "created": false
      },
      "createSample": null,
      "g": {
        "id": "SUB-42",
        "code": "initial code"
      },
      "updateSample": {
        "code": "updated code"
      }
    }
  }
}

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

Внимание!

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

Команды Many#

Команды пакета ориентированы на работы с единственным экземпляром сущности. Для выполнения множественных действий команды create, update, updateOrCreate и delete имеют аналоги Many, принимающие массив аргументов.

Существенным отличием команд Many является упрощение результата выполнения:

  • createMany${наименование класса модели} результатом является [String] идентификаторов созданных сущностей;

  • updateMany${наименование класса модели} результатом является константа success;

  • deleteMany${наименование класса модели} результатом является константа success;

  • updateOrCreateMany${наименование класса модели} результатом является константа _UpdateOrCreateManyResponse.

Команды в качестве входящего аргумента принимают массив объектов, соответствующих специфике команды. Массив результата команд createMany и updateOrCreateMany соответствует порядку входящего аргумента. Результат выполнения команд можно использовать с другими командами через ref: с указанием индекса команды в массиве результата после имени команды в [].

Пример:

mutation{
  packet{
    m: createManySample(input:[
      {code: "sample 1"},
      {code: "sample 2"}
    ])
    
    g1: getSample(id: "ref:m[0]") { id code }
    
    g2: getSample(id: "ref:m[1]") { id code }
    
  }
}

Результат:

{
  "data": {
    "packet": {
      "m": [
        "7139554145819230209",
        "7139554145819230210"
      ],
      "g1": {
        "id": "7139554145819230209",
        "code": "sample 1"
      },
      "g2": {
        "id": "7139554145819230210",
        "code": "sample 2"
      }
    }
  }
}

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

mutation{
  packet{
    createManySample(input:[ { id: "1" }, { id: "2" } ])
    updateManySample(input: [ 
      {param: { id: "1" code: "1" }}, 
      {param: { id: "2" code: "2" }}
    ])
    updateOrCreateManySample(input: [
      {
        param: { id: "1" code: "10" } exist: { update: { } }
      },
      {
        param: { id: "2" code: "20" } exist: { update: { } }
      }
    ]) { id created }
    deleteManySample(input: [
      {id: "1", compare: {code: "1"}}, 
      {id: "2", compare: {code: "2"}}
    ])
  }
}

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

{
  "data": {
    "packet": {
      "createManySample": [
        "1",
        "2"
      ],
      "updateManySample": "success",
      "updateOrCreateManySample": [
        {
          "id": "1",
          "created": false
        },
        {
          "id": "2",
          "created": false
        }
      ],
      "deleteManySample": "success"
    }
  }
}

Пояснение: команда updateOrCreateManySample не создает сущности, так как они были созданы ранее, и в варианте пустого объекта update поля exist не выполняет изменение поля code, что фиксируется при выполнении команды deleteManySample заполнением параметра compare.

Для команд Many допускается условное выполнение. Результат выполнения пропущенной команды в результате будет null.

Пример: поля пакета для работы с продуктом#

type _Packet {
    aggregateVersion: Long
    isIdempotenceResponse: Boolean
    # ...
    createProduct(input: _CreateProductInput!): Product
    updateProduct(input: _UpdateProductInput!): Product  
    deleteProduct(id: ID!): String
    getProduct(id: ID!): Product
    tryLockProduct(input: _TryLockInput!): _LockOutput
    unlockProduct(input: _UnlockInput!): _LockOutput
    # ...
}

Запрос#

Запрос в схеме представлен объектом с наименованием _Query. Объект включает в себя следующие поля:

  • merge(limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _MergedEntitiesCollection! — для слияния поисковых запросов, где:

    • limit — ограничение на количество элементов;

    • offset — смещение;

    • sort — сортировка.

  • search${наименование класса модели}(cond: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]) : _EC_${наименование класса модели}! — для каждого класса модели, для поиска сущности соответствующего класса, где:

    • cond — условие фильтрации в грамматике строковых выражений;

    • limit — ограничение на количество элементов;

    • offset — смещение;

    • sort — сортировка.

  • search${наименование класса модели}History(cond: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]) : _EC_${наименование класса модели}! — для каждого класса модели из цепочки наследования, содержащей историцируемые поля, для поиска по истории изменения сущности, где:

    • cond — условие фильтрации в грамматике строковых выражений;

    • limit- ограничение на количество элементов;

    • offset — смещение;

    • sort — сортировка.

  • selectionBy${наименование класса модели}(cond: String, distinct: Boolean, group: [String!], groupCond: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _SEC_${наименование класса модели}! — для каждого класса модели, для выборки свойств на основе поиска сущности одного типа, distinct и groupBy поисков, где:

    • cond — условие фильтрации в грамматике строковых выражений;

    • limit — ограничение на количество элементов;

    • offset — смещение;

    • sort — сортировка,

    • distinct — признак выбора уникальных картежей

    • group — условия группировки в грамматике строковых выражений;

    • groupCond — условия фильтрации после группировки (аналог having) в грамматике строковых выражений.

  • getState${наименование класса модели}(id: String!, date: _OffsetDateTime!) — для каждого класса модели из цепочки наследования, содержащей историцируемые поля, возвращает состояние историцируемых атрибутов сущности на заданный момент времени, где:

    • id — идентификатор сущности для которой определяется состояние;

    • date — время на которое определяется состояние сущности.

  • getStates${наименование класса модели}(id: String!, timeFrom: _OffsetDateTime, timeTo: _OffsetDateTime, limit: Int, offset: Int) — для каждого класса модели из цепочки наследования, содержащей историцируемые поля, возвращает список состояний сущности, где:

    • id — идентификатор сущности для которой определяется состояние;

    • timeFrom — ограничение временного диапазона поиска снизу;

    • timeTo — ограничение временного диапазона поиска сверху;

    • limit — ограничение на количество элементов;

    • offset — смещение.

  • get${наименование класса модели}History(id: String!, timeFrom: _OffsetDateTime, timeTo: _OffsetDateTime, limit: Int, offset: Int) — для каждого класса модели из цепочки наследования, содержащей историцируемые поля, возвращает список изменений сущности, где:

    • id — идентификатор сущности для которой определяется состояние;

    • timeFrom — ограничение временного диапазона поиска снизу;

    • timeTo — ограничение временного диапазона поиска сверху;

    • limit — ограничение на количество элементов;

    • offset — смещение.

  • resolveReferences(referenceType: String!, ids: [ID!]!): [_Reference!]! - для разрешения ссылок, где:

    • referenceType - тип ссылок, которые будут возвращены в списке;

    • ids - идентификаторы сущностей, для которых необходимо разрешить ссылки.

  • multisearch${наименование класса модели}(cond: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!], ctx: String) : _EC_${наименование класса модели}! — для каждого класса модели, для межшардового поиска сущности соответствующего класса, где:

    • cond — условие фильтрации в грамматике строковых выражений;

    • limit — ограничение на количество элементов;

    • offset — смещение;

    • sort — сортировка;

    • ctx - контекст мультипоиска, сериализованный в строку, пример контекста можно найти здесь

  • multimerge(limit: Int, offset: Int, sort: [_SortCriterionSpecification!], ctx: String): _MergedEntitiesCollection! — для слияния поисковых запросов, где:

    • limit — ограничение на количество элементов;

    • offset — смещение;

    • sort — сортировка;

    • ctx - контекст мультипоиска, сериализованный в строку, пример контекста можно найти здесь

Пример: поля запроса для работы с продуктом.#

type _Query {
    merge(limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _MergedEntitiesCollection!
    # ...
    searchProduct(cond: String, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _EC_Product!
    selectionByProduct(cond: String, distinct: Boolean, limit: Int, offset: Int, sort: [_SortCriterionSpecification!]): _SEC_Product!
    # ...
    resolveReferences(referenceType: String!, ids: [ID!]!): [_Reference!]!
}

Мутация#

Мутация в схеме представлена объектом с наименованием _Mutation. Данный объект содержит единственное поле packet(aggregateVersion: Long, idempotencePacketId: String): _Packet, где:

  • aggregateVersion — версия агрегата для оптимистической блокировки. Если аргумент не указан, то проверка выполняться не будет. Версия агрегата после выполнения пакета может быть получена через запрос поля aggregateVersion типа объекта _Packet;

  • idempotencePacketId — ключ идемпотентности пакета. Если аргумент не указан, то пакет не будет проверяться на идемпотентность.

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

Тип объекта _Mutation в схеме:

type _Mutation {
    packet(aggregateVersion: Long, idempotencePacketId: String): _Packet
}

Пример итоговой схемы#

Пример схемы GraphQL можно увидеть во вложенном файле graphql-schema-big.

Виды запросов#

Поиск#

Для поиска сущностей определенного типа необходимо запрашивать search-поле, соответствующее классу модели.

Пример: поиск продукта (Product) с идентификатором, равным "1", с запросом кода (code).

{
  searchProduct(cond: "it.$id == '1'") {
    elems {
      code
    }
  }
}

С помощью аргументов limit, offset можно управлять настройками пагинации. С помощью аргумента sort можно задавать настройки сортировки. А запросив поле count, можно получить общее количество элементов, удовлетворяющих условию поиска.

Ниже приведен пример поиска продуктов (Product) с кодом (code):

  • соответствующим шаблону product%;

  • с запросом идентификатора и кода;

  • отсортированных по коду по возрастанию, а затем по идентификатору по убыванию;

  • с отображением только 10 элементов, пропуская первые 20;

  • с запросом общего количества продуктов, удовлетворяющих условию поиска.

{
  searchProduct(cond: "it.code $like 'product%'", sort: [{crit: "it.code"}, {crit: "it.$id", order: DESC}], limit: 10, offset: 20) {
    elems {
      id
      code
    }
    count
  }
}

С помощью аргументов cond, limit, offset, sort для свойств-коллекций примитивов/ссылок можно также управлять настройками фильтрации, пагинации и сортировки, а запросив поле count, получить общее количество элементов, удовлетворяющих условию фильтрации.

Ниже приведен пример поиска продукта (Product):

  • с идентификатором, равным 1;

  • с запросом состояний (states):

    • со значением, соответствующим шаблону state%;

    • отсортированных по значению по возрастанию;

    • с отображением только 10 элементов, пропуская первые 20;

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

  • с запросом сервисов (services):

    • с кодом (code) соответствующим шаблону service%;

    • с запросом идентификатора и кода;

    • отсортированных по коду по возрастанию, а затем по идентификатору по убыванию;

    • с отображением только 10 элементов, пропуская первые 20;

    • с запросом общего количества продуктов, удовлетворяющих условию фильтрации.

{
  searchProduct(cond: "it.$id == '1'") {
    elems {
      states(cond: "it $like 'state%'", sort: {crit: "it"}, limit: 10, offset: 20) {
        elems
        count
      }
      services(cond: "it.code $like 'service%'", sort: [{crit: "it.code"}, {crit: "it.$id", order: DESC}], limit: 10, offset: 20) {
        elems {
          id
          code
        }
        count
      }
    }
  }
}

С помощью аргументов alias для свойств-ссылок и elemAlias для свойств-коллекций ссылок можно задавать псевдоним для ссылки/элемента коллекции ссылок, который можно использовать для фильтрации вложенных коллекций.

Ниже приведен пример поиска продукта (Product):

  • с идентификатором, равным 1;

  • с запросом документа (document):

    • с запросом состояний (states) со значением, соответствующим шаблону код документа (code) + state%;

  • с запросом сервисов (services) с запросом состояний (states) со значением, соответствующим шаблону код сервиса (code) + state%.

{
  searchProduct(cond: "it.$id == '1'") {
    elems {
      document(alias: "document") {
        states(cond: "it $like @document.code + 'state%'") {
          elems
        }
      }
      services(elemAlias: "service") {
        elems {
          states(cond: "it $like @service.code + 'state%'") {
            elems
          }
        }
      }
    }
  }
}

С помощью фрагментов по интерфейсам можно запрашивать детализацию типа, то есть запрашивать дополнительные поля для классов наследников.

Ниже приведен пример поиска продуктов (Product):

  • с кодом (code), соответствующим шаблону product%;

  • с запросом идентификатора и кода;

  • с запросом даты начала действия (beginDate) в случае, если продукт является депозитом (Deposit).

{
  searchProduct(cond: "it.code $like 'product%'") {
    elems {
      id
      code
      ... on Deposit {
        beginDate
      }
    }
  }
}

Битые ссылки#

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

Запрос:

    searchProduct(cond: "root.$id == '${product1Id}'") {
        elems {
            relatedProduct {
                code
            }
            request {
                initiator {
                    firstName
                    lastName
                }
            }
        }
    }

Ответ:

  "errors": [
    {
      "message": "Reference of type Product with id = nonexistentId is invalid",
      "path": [
        "searchProduct",
        "elems",
        0,
        "relatedProduct"
      ],
      "extensions": {
        "classification": "InvalidData"
      }
    }
  ],
  "data": {
    "searchProduct": {
      "elems": [
        {
          "relatedProduct": null,
          "request": {
            "initiator": {
              "firstName": "Vasya",
              "lastName": "Vasiliev"
            }
          }
        }
      ]
    }
  }

Поиск уникальных значений#

Для поиска уникальных значений (картежей) необходимо запрашивать selectionBy-поле, соответствующее классу модели, задав distinct: true.

Пример: поиск уникальных пар code, name продукта (Product):

{
  selectionByProduct(cond: "it.code $like 'abc%'", distinct: true) {
    elems {
      code
      name
    }
  }
}

Запрос с группировкой#

Для запроса с группировкой необходимо запросить selectrionBy-поле, соответствующее классу модели, задав group атрибут.

Пример: группировка выборки по code и name с подсчетом количества записей и дополнительной фильтрацией записей, количество которых более трех:

{
  selectionByProduct(cond: "it.code $like 'abc%'", group: ["it.code", "it.name"], groupCond: "it.$id.$count > 3") {
    elems {
      code
      name
      count: _getInt(expression: "it.$id.$count")
    }
  }
}

Слияние запросов#

Для слияния поисковых запросов по несвязанным наследованием типам, необходимо запрашивать merge-поле. Затем с помощью встроенных фрагментов по интерфейсам, соответствующим классам модели, запросы по которым нужно слить, и директивы mergeReqSpec необходимо оформить поисковые запросы, которые нужно слить, по следующим правилам:

  • Для поисковых запросов можно задать только условие поиска в аргументе cond директивы mergeReqSpec.

  • В аргументах limit и offset поля можно задать настройки пагинации, действующие на все слияние.

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

  • С помощью запроса поля count можно получить общее количество элементов.

Ниже приведен пример слияния запросов:

  • Поиск продуктов (Product) с кодом (code), соответствующим шаблону product%; с запросом идентификатора и кода; с запросом даты начала действия (beginDate) в случае, если продукт является депозитом (Deposit).

  • Поиск сервисов (Service) с кодом (code), соответствующим шаблону service%; с запросом идентификатора и состояний (states).

  • Поиск документа (Document) с идентификатором в диапазоне [1, 2, 3]; с запросом продукта (product) ( с запросом кода (code). С условиями:

  • с сортировкой по коду по возрастанию, а затем по идентификатору по убыванию;

  • с отображением только 10 элементов, пропуская первые 20;

  • с запросом общего количества продуктов, удовлетворяющих условию поиска.

{
  merge(sort: [{crit: "it.code"}, {crit: "it.$id", order: DESC}], limit: 10, offset: 20) {
    elems {
      ... on Product @mergeReqSpec(cond: "it.code $like 'product%'") {
        id
        code
        ... on Deposit {
          beginDate
        }
      }
      ... on Service @mergeReqSpec(cond: "it.code $like 'service%'") {
        id
        states {
          elems
        }
      }
      ... on Document @mergeReqSpec(cond: "it.$id $in ['1', '2', '3']") {
        product {
          code
        }
      }
    }
    count
  }
}

Запрос состояния историцируемой сущность на момент времени#

{
  getStateProduct(id: "7067525912085069825", date: "2022-02-22T16:32:00Z") {
    code
    name
  }
}

Запрос списка состояний историцируемой сущности#

{
  getStatesProduct(id: "7067525912085069825") {
    elems {
      sysHistoryTime
      sysHistNumber
      code
      sysCodeUpdated
      name
      sysNameUpdated
    }
    count
  }
}

Запрос списка изменений сущности#

{
  getProductHistory(id: "7067525912085069825") {
    elems {
      sysHistoryTime
      sysHistNumber
      code
      sysCodeUpdated
      name
      sysNameUpdated
    }
    count
  }
}

Межшардовый поиск (мультипоиск)#

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

    {
      multisearchProductParty(cond: "it.code!=null", limit: 10, sort: [{crit:"it.name"}], ctx: "{\"shards\":{\"shard1\":3, \"shard2\":7}, \"offset\":0, \"limit\":10,\"checksum\":138034844,\"lastPageSize\":10}"){
        elems{
          code
          name
        }
        count
        ctx
      }
    }

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

    {
      multimerge(sort: [{crit: "it.code"}, {crit: "it.$id", order: DESC}], limit: 10, ctx: "{\"shards\":{\"shard2\":4,\"shard1\":6},\"offset\":0,\"limit\":10,\"checksum\":2308350222,\"lastPageSize\":10}") {
        elems {
          ... on ProductParty @mergeReqSpec(cond: "it.code!=null") {
            id
            code
          }
          ... on RequestInst @mergeReqSpec(cond: "it.code!=null") {
            id
          }
        }
        count
        ctx
      }
    }

Разрешение ссылок#

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

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

Пример: разрешение ссылок на продукты (Product) с идентификаторами равными 1, 2 и 3; с запросом кода (code).

{
    resolveReferences(referenceType: "_R_Product", ids: ["1", "2", "3"]) {
        ... on _R_Product {
            entity {
                code
            }
        }
    }
}

Пакет#

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

Пример: создание продукта (Product)

Запрос:

mutation {
  packet {
    createProduct(input: {
      code: "product1"
    }) {
      id
      code
    }
  }
}

Результат:

{
  "data": {
    "packet": {
      "createProduct": {
        "id": "6934251168182108161",
        "code": "product1"
      }
    }
  }
}

В пакете могут быть выполнены операции над несколькими сущностями одного агрегата. Если в классе модели используется автоматическое формирование идентификатора, то для указания ссылки на создаваемый в пакете объект используется специализированный синтаксис описания идентификатора ref:{псевдоним/наименование поля создания объекта}.

Пример: создание продукта (Product) и связанного с ним сервиса (Service)

Запрос:

mutation {
  packet {
    createProduct(input: {
      code: "product1"
    }) {
      id
    }
    createService(input: {
      product: "ref:createProduct"
      code: "service1"
    }) {
      id
      product {
        id
        code
      }
    }
  }
}

Результат:

{
  "data": {
    "packet": {
      "createProduct": {
        "id": "6934253088032489473"
      },
      "createService": {
        "id": "6934253088032489474",
        "product": {
          "id": "6934253088032489473",
          "code": "product1"
        }
      }
    }
  }
}

Пример: создание продукта (Product) и связанного с ним сервиса (Service) (с использованием псевдонима)

mutation {
  packet {
    product1: createProduct(input: {
      code: "product1"
    }) {
      id
    }
    createService(input: {
      product: "ref:product1"
      code: "service1"
    }) {
      id
      product {
        id
        code
      }
    }
  }
}

Результат:

{
  "data": {
    "packet": {
      "product1": {
        "id": "6934253088032489473"
      },
      "createService": {
        "id": "6934253088032489474",
        "product": {
          "id": "6934253088032489473",
          "code": "product1"
        }
      }
    }
  }
}

Пример: создание и обновление продукта (Product) с чтением промежуточных состояний

mutation {
  packet {
    product1: createProduct(input: {code: "product1"}) {
      id
      code
    }
    product1_afterCreate: getProduct(id: "ref:product1") {
      id
      code
    }
    product1_updated: updateProduct(input: {id: "ref:product1", code: "product1_new"}) {
      id
      code
    }
    product1_afterUpdate: getProduct(id: "ref:product1") {
      id
      code
    }
  }
}

Результат:

{
  "data": {
    "packet": {
      "product1": {
        "id": "6934262438176292865",
        "code": "product1"
      },
      "product1_afterCreate": {
        "id": "6934262438176292865",
        "code": "product1"
      },
      "product1_updated": {
        "id": "6934262438176292865",
        "code": "product1_new"
      },
      "product1_afterUpdate": {
        "id": "6934262438176292865",
        "code": "product1_new"
      }
    }
  }
}

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

Пример: создание двух независимых продуктов (Product)

mutation {
  packet1: packet {
    createProduct(input:{ code: "product1" }) {
      id
    }
  }
 
  packet2: packet {
    createProduct(input:{ code: "product2" }) {
      id
    }
  }
}

Результат:

{
  "data": {
    "packet1": {
      "createProduct": {
        "id": "6934264250652491777"
      }
    },
    "packet2": {
      "createProduct": {
        "id": "6934264250652491778"
      }
    }
  }
}

Для обеспечения идемпотентного вызова пакета необходимо указать атрибут idempotencePacketId поля packet. Опциональное поле isIdempotenceResponse определяет состояние выполнения пакета.

Пример: создание продукта (Product) в идемпотентном вызове с проверкой состояния выполнения пакета

mutation {
  packet(idempotencePacketId: "1") {
    isIdempotenceResponse
 
    createProduct(input: {code: "product1"}) {
      id
    }
  }
}

Результат:

{
  "data": {
    "packet": {
      "isIdempotenceResponse": false,
      "createProduct": {
        "id": "6934265174070460417"
      }
    }
  }
}

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

{
  "data": {
    "packet": {
      "isIdempotenceResponse": true,
      "createProduct": {
        "id": "6934265174070460417"
      }
    }
  }
}

Значение isIdempotenceResponse, равное True, указывает на то, что операция не была выполнена и получен ранее созданная сущность. Совпадение идентификаторов подтверждают это. Важно отметить, что идемпотентными являются операции создания и изменения сущности, но чтение данных выполняется всегда.

Пример: создание продукта (Product) и сервиса (Service) с последующим удалением сервиса в идемпотентном вызове

mutation {
  packet(idempotencePacketId: "1") {
    createProduct(input: {code: "product1"}) {
      id
    }
    createService(input: {product: "ref:createProduct", code: "service1"}) {
      id
    }
    deleteService(id: "ref:createService")
  }
}

Результат:

{
  "data": {
    "packet": {
      "createProduct": {
        "id": "6934272192047022081"
      },
      "createService": {
        "id": "6934272192047022082"
      },
      "deleteService": "success"
    }
  }
}

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

{
  "errors": [
    {
      "message": "Ошибка обработки команды id = 'createService#GET4GQL', name = 'get': Не найден экземпляр типа 'Service' с идентификатором '6934272192047022082'",
      "locations": [],
      "extensions": {
        "classification": "OBJECT_NOT_FOUND"
      }
    }
  ],
  "data": {
    "packet": null
  }
}

Для демонстрации работы оптимистической блокировки в пакете на первом шаге создается продукт (Product) с запросом текущей версии агрегата:

mutation {
  packet {
    aggregateVersion
     
    createProduct(input: {code: "product1"}) {
      id
    }
  }
}

Результат:

{
  "data": {
    "packet": {
      "aggregateVersion": 1,
      "createProduct": {
        "id": "6934266763208359937"
      }
    }
  }
}

На втором шаге выполняется обновление объекта с указанием текущей версии и запросом новой версии:

mutation {
  packet(aggregateVersion: 1) {
    aggregateVersion
     
    updateProduct(input: {id: "6934266763208359937", code: "product1_new"}) {
      id
    }
  }
}

Результат:

{
  "data": {
    "packet": {
      "aggregateVersion": 2,
      "updateProduct": {
        "id": "6934266763208359937"
      }
    }
  }
}

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

{
  "errors": [
    {
      "message": "Version 1 required but found 2 for object 6934266763208359937#local.coreaslib.sdk.jpa.Product",
      "locations": [],
      "extensions": {
        "classification": "AGGREGATE_VERSION_EXCEPTION"
      }
    }
  ],
  "data": {
    "packet": null
  }
}

Другие примеры#

Работа с датами#

Поиск рейсов из Москвы во Владивосток между 11.09.2023 и 17.09.2023, которые отправляются до 12:00.

{
  searchFlight(cond: """
        it.departureAirport.city.code == 'MOW' &&
        it.arrivalAirport.city.code == 'VVO' &&
        it.departureDate.$date $between (D2023-09-11, D2023-09-17) &&
        it.departureDate.$time <= T12:00
     """) {
    elems {
        no
        departureDate
        arrivalDate
        departureAirport {
            code
            city {
                name
            }
        }
        arrivalAirport {
            code
            city {
                name
            }
        }
    }
  }
}

Примечание

Предполагается, что даты хранятся с типом LocalDateTime в часовой зоне аэропорта.