dataspace_tanks_tutorial
Учебное руководство по Platform V DataSpace на примере системы учета ГСМ (Нефтебаза)
Введение
Данное руководство является частью учебного курса по инструментам LowCode-платформы от СберТех - Platform V DataSpace & Flow. В нем на примере модели предметной области нефтебазы дается простое и понятное объяснение основных концепций предметно-ориентированного проектирования, работы с языком запросов GraphQL, а также специализированного инструмента DataSpace для описания бизнес-логики - строковых выражений. Руководство предназначено для широкой аудитории не требует каких-либо узкоспециализированных навыков и знания языков программирования. Однако для лучше понимания рекомендуется предварительно ознакомиться с материалом "Быстрый старт Platform V DataSpace".
Модель предметной области
Основные объекты модели:
- Агрегат
- Класс
- Класс-справочник
- Перечисление
- Embeddable-класс
- Абстрактный класс
Между объектами можно строить различные типы связей
Свойства (поля)
- Обязательность
- Версионирование
Способы генерации ID:
- АВТО (SNOWFLAKE)
- Смешанный (AUTO_ON_EMPTY)
- Пользовательский
Скалярные типы данных:
- String
- Unicode String
- Text
- BigDecimal
- Integer
- Short
- Long
- Byte
- Boolean
- Character
Событийная модель:
- наблюдатель
- контейнер статусов
- статус
Предметно-ориентированное проектирование
Получить расширенную информацию о, том что такое предметно-ориентированное проектирование (DDD) можно в разделе Дополнительная информация руководства "Быстрый старт Platform V DataSpace".
Создание проекта DataSpace
Далее будет пошагово рассматриваться созданием модели предметной области на примере учета ГСМ (Нефтебаза), готовую модель можно скачать по ссылке.
Агрегаты, классы, свойства
Агрегат - группа тесно связанных сущностей, с явно выделенным корнем.
Агрегат определяет транзакционную границу, в рамках одной транзакции мы можем работать только с одной сущностью, ее данными
Агрегат может содержать в себе несколько классов, причем один из них будет корневым.
Создание агрегата и корневого класса Tank
Свойства:
- name String обязательный
- currentVal Integer
- location (тип будет указан позже)
- tankType (тип будет указан позже)
Способ заполнения ID - SnowFlake
Создание embeddable-класса Coordinates
Embeddable-классы (встроенные классы) применяется для группировки нескольких полей примитивов в одно. В данном проекте встроенный класс Coordinates используется для хранение значений долготы и широты
Свойства:
- latitude String широта
- longitude String долгота
После создания встроенного класса Coordinates его можно использовать в качестве типа для полей других классов.
Укажем данный класс в качестве типа для поля location класса Tank:
Создание классов-справочников
Класс-справочник, в отличии от агрегата который является атомарной единицей и у которого должны быть "мягкие ссылки" и который можно перенести в другую БД, на значения КС можно ссылаться как на системную информацию
TankType (Типы цистерн)
- name String
- description String
- maxVal Integer
- measure (тип будет указан позже)
Measure (Единицы измерения)
- name String
- description String
Связываем перетаскиванием поле measure класса-справочника CisternalType с классом-справочником Measure Для поля tankType класса Tank типом указываем класс-справочник TankType
Добавление класса к агрегату Tank
Добавляем к агрегату Tank новый класс TankOperation
Тип связи - Композиция, класс-владелец Tank (необходимо выбрать, так как классов в агрегате может быть много) тип связи - один ко многим
В новом классе TankOperation будет новое свойство tank, а в классе Tank поле tankOperationList если переносить на базы данных это аналог внешнего ключа
!
Добавляем еще свойства TankOperation:
- val Integer
- operDateTimeUtc OffsetDateTime (чтобы не было проблем с часовыми поясами)
- kind (тип будет указан позже)
Перечисление ENUM
Перечисления предназначены для хранения ограниченного набора значений (коллекции)
Cоздаем ENUM TankOperKind - тип операции
Значения:
- INPUT
- OUTPUT
для поля kind класса TankOperation указываем типом перечисление TankOperKind
Статусная модель
Каждая сущность может быть представлена разными статусами для разных наблюдателей Нефтебаза, операция для внешних пользователей (2-3 статуса) и для внутренних (+куча другой инфы)
Переименуем статус stausCode в NEW и добавим еще статусы:
- CANCELED
- APPRUVED
- DONE
Успешная проверка модели перед выпуском:
Выпуск модели
Основы языка запросов GraphQL
GraphQL-конструктор DataSpace
После выпуска модели в интерфейсе DataSpace появляются три новых вкладки:
- GraphQL конструктор - интерактивный playground для выполения GraphQL-запросов
- Детали - информация о доступных API endpoints DataSpace
- Разрешения - настройка параметров доступа для GraphQL-запросов
GraphQL конструктор
Вкладка SHEMA - просмотр и скачивание GraphQL-схемы модели предметной области
Вкладка DOCS - просмотр информации о доступных GraphQL-запросах операция + тип = имя доступные запросы или мутации:
Настройки GraphQL-конструктора:
Подсказки по коду: Ctrl + пробел
Типы данных GraphQL
- скалярные (примитивные)
- кастомные
Типы запрос GraphQL
- Query - получение данных
- Mutation (create, update, delete)
- Subscription (подписки) - отслеживание изменений данных на оснвое Websocket
Простой запрос (query) - получение списка всех цистерн
query searchTank { searchTank { elems{ id currentVal location { longitude latitude } } }}
Результат:
{ "data": { "searchTank": { "elems": [] } }}
Мы получаем пустой массив elems так как в базе данных пока нет ни одной записи для сущности Tank
Мутации (mutation) - создание, обновление, удаление данных
Выполним две мутации для наполнения классов-справочников Measure и TankType
Пакеты
Мутации могут выполняться в составе пакета Пакет = транзакция базы данных Команды выполняются последовательно Если какая-то операция пакета на выполняется весь пакет откатывается транзакция завершается
В DataSpace есть два типа пакетов:
- packet
- dictionaryPacket
Мутация - Создание единицы измерения в классе-справочнике Measure
мутация updateOrCreateMeasure проверяет есть ли уже данная запись, если да - она обновляет ее, если нет - создает.
created - возвращает true если запись была создана returning позволяет вернуть свойства созданной сущности сразу после операции (доступны поля сущности)
mutation CreateMeasure { dictionaryPacket { updateOrCreateMeasure( input: { name: "M3", description: "Метр кубический", id: "m3" } ) { created returning{ id } } }}
Рузультат:
{ "data": { "dictionaryPacket": { "updateOrCreateMeasure": { "created": true, "returning": { "id": "m3" } } } }}
Алиасы (псевдонимы)
GraphQL позволяет создавать алиасы для пакетов и мутаций. Это бывает например необходимо когда мы хотим выполнить в одном пакете несколько одинаковых операций с одинакомыми именами. Без алиасов в ответе мы бы получали одинаковые имена в JSON поэтому GraphQL схлопывал бы их один. Алиасы позволяют этого избежать:
mutation CreateMeasure { p1: dictionaryPacket { t1:updateOrCreateMeasure( input: { name: "Т", description: "Тонна", id: "t" } ) { created returning{ id name description } }
t2:updateOrCreateMeasure( input: { name: "Г", description: "Галлон", id: "g" } ) { created returning{ id name description } }
}}
Рузультат:
{ "data": { "p1": { "t1": { "created": true, "returning": { "id": "t", "name": "Т", "description": "Тонна" } }, "t2": { "created": true, "returning": { "id": "g", "name": "Г", "description": "Галлон" } } } }}
Мутация - Добавление типа цистерны в класс-справочник TankType
mutation createTankType {
p1: dictionaryPacket { smallTank: updateOrCreateTankType( input: { id: "smallTank" name: "Малая цистерна" measure: "m3" maxVal: 1000 } ) { created } }
p2: dictionaryPacket { middleTank: updateOrCreateTankType( input: { id: "middleTank" name: "Средняя цистерна" measure: "m3" maxVal: 3000 } ) { created } }
p3: dictionaryPacket { bigTank: updateOrCreateTankType( input: { id: "bigTank" name: "Большая цистерна" measure: "m3" maxVal: 5000 } ) { created } }}
Результат:
{ "data": { "p1": { "smallTank": { "created": true } }, "p2": { "middleTank": { "created": true } }, "p3": { "bigTank": { "created": true } } }}
Создание цистерны
Важно что используем уже packet, а не dictionaryPacket:
mutation createTank { packet{ createTank(input:{ name:"Цистерна 1" tankType:"smallTank" currentVal:0 }) { id } }}
Результат запроса:
{ "data": { "packet": { "createTank": { "id": "7426871612607692801" } } }}
7426871612607692801 - идентификатор цистерны, сгенерированный методом SNOWFLAKE
Переменные в GraphQL-запросах
GraphQL позволяет использовать в запросах и мутациях параметры. Это дает возможность передавать данные в контекст запросов из внешних систем. Значения параметров передаются во вкладке Query variables GraphQL-конструктора
Мутация - создание цистерны с использованием переменных
mutation createTank ( $name: String! $tankType: ID!){ packet{ createTank(input:{ name:$name tankType:$tankType currentVal:0 }) { id } }}
Query variables:
{ "name": "Цистерна 2", "tankType": "middleTank"}
Результат запроса:
{ "data": { "packet": { "createTank": { "id": "7426873618357420033" } } }}
Query-запрос - получение списка цистерн
query searchTank { searchTank { elems{ id name currentVal } }}
У запросов в DataSpace могут быть дополнительные параметры:
-
cond - условие (аналого WHERE SQL)
-
limit - ограничение количества элементов в результате запроса
-
offset - пропустить указанное поличество элементов перед началом вывода
-
sort - сортировка (ASC, DESC)
-
cond: String — условие фильтрации в грамматике строковых выражений;
-
limit: Int — ограничение на количество элементов;
-
offset: Int — смещение;
-
sort: [_SortCriterionSpecification!] — сортировка;
Строковые выражения DataSpace
Для cond могут применяются cтроковые выражения - специальный синтаксис DataSpace для описания различных параметров поиска, который удобно использовать для описания сложной бизнес-логики без необходимости описания ее в backend
it - текущий элемент по которому итерируемся, аналог this id - какое-либо поле, например id
Примеры:
Поиск цистерны по определенному id
query searchTank { searchTank (cond: "it.id == '7426871612607692801'") { elems{ id name currentVal } }}
Поиск цистерны определенного типа
query searchTank { searchTank (cond: "it.tankType.id == 'smallTank'") { elems{ id name } }}
Получение всех типов цистерн, у которых единицей измерения объема является 'l'
query searchTankType { searchTankType (cond:"it.measure.id == 'l'") { elems { id description maxVal measure{ id description } } }}
Получение всех цистерн, у которых единицей измерения является "m3" Условие с использованием свойства вложенного объекта
query searchTank { searchTank (cond:"it.tankType.measure.id == 'm3'") { elems{ id name } }}
Дополнительные примеры использования строковых выражений:
использование < > == != операций
cond:"it.maxVal > 5000"
условие "Отрицание":
!${условие} (например, !it.services.$exists);
логическое "И":
${условие} && ${условие} (например, it.services.$exists && it.product != null);
логическое "ИЛИ":
${условие} || ${условие} (например, it.services.$exists || it.product != null).
Использование методов строковых выражений, условие: код продукта равен 'product1' независимо от регистра:
it.code.$lower == 'product1'
Использование методов строковых выражений, условие: суммарное время выполнение всех сервисов продукта, код которых начинается с кода продукта, не превышает 10 (при поиске продуктов).
it.services{cond = it.code $like root.code + '%'}.executionTime.$sum <= 10
Условие: код продукта равен одному из значений 'product1', 'product2' или 'product3'.
it.code == any(['product1', 'product2', 'product3])
Комплексный пример по строковым выражениям
Показывает гибкие возможности построение запросов DataSpace. Запрос возвращает список цистерн, у которых значение параметра tankType.measure.id начинается с литеры "M" вне зависимости от регистра.
При этом также задействованы параметры offset (пагинация), limit (ограничение количества элементов в выводе) и sort (сортировка результатов по определенному полю, в данном случае name)
Также важно отметить конструкцию @strExpr (директива), позволяющую избежать проблем с обработкой GraphQL-запроса, когда переменная $measureSearchStr используется только в строковом выражении.
# Write your query or mutation herequery searchTank ( $measureSearchStr: String $offset: Int $limit: Int) { searchTank ( cond:"it.tankType.measure.id.$upper $like ${measureSearchStr}.$upper + '%'" offset:$offset limit:$limit sort: {crit: "it.name", order:DESC} ) @strExpr(string:$measureSearchStr) { elems{ id name } }}
{ "measureSearchStr": "M", "offset": 1, "limit": 2}
{
"data": {
"searchTank": {
"elems": [
{
"id": "7426874138048462849",
"name": "Цистерна 2"
},
{
"id": "7426871612607692801",
"name": "Цистерна 1"
}
]
}
}
}
Мутация - изменение количества топлива в цистерне
DataSpace позволяет обеспечить дополнительные проверки при выполнении запрос по изменению данных. Особенно важно это в высоконагруженных приложениях, сервисах.
В данном примере показано изменение уровня топлива в цистерне. При этом проводятся дополнительные проверки на корректность выполнения данной операции. В частности с помощью строкового выражения проверяется не будет ли значение уровня топлива в цистерне после операции меньше 0. Также помимо изменение значения уровня топлива в пакете происходит создание записи об операции.
Отдельно стоит указать параметр lock:WAIT - он позволяет выполнять блокировку, дает возможность бесконфликтно выполнять множественные изменения данных, обеспечивая их очередность.
mutation decreaseTankVolume( $tankId: ID! $val: BigDecimal! $opDate: _OffsetDateTime!) { packet { getTank( id: "find: it.id == ${tankId} && it.currentVal-${val}-it.tankType.minVal>=0" failOnEmpty: true lock: WAIT ) { id }
updateTank( input: { id: $tankId } inc: { currentVal: { value: $val, negative: true } } ) { id currentVal }
createTankOperation( input: { tank: $tankId val: $val transfer: { entityId: "ttt" } kind: OUTPUT operDateTimeUtc: $opDate } ) { id operDateTimeUtc } }}
Идемпотентность
Для обеспечения идемпотентного вызова пакета необходимо указать атрибут idempotencePacketId поля packet
Пример создания продукта (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, указывает на то, что операция не была выполнена и получен ранее созданная сущность. Совпадение идентификаторов подтверждают это. Важно отметить, что идемпотентными являются операции создания и изменения сущности, но чтение данных выполняется всегда.
Ссылки
Platform V DataSpace:
https://platformv.sbertech.ru/products/instrumenty-razrabotchika/dataspace
Строковые выражения DataSpace:
https://client.sbertech.ru/docs/public/APT/1.13.0/DSPC/1.13.0/documents/string-expressions/index.html
Протокол GraphQL:
https://client.sbertech.ru/docs/public/APT/1.13.0/DSPC/1.13.0/documents/dataspace-core-graphql-protocol/index.html
Канал Platform V на Rutube:
https://rutube.ru/channel/30199350/
Инструменты Сбера для разработчиков:
https://developers.sber.ru/
TG-канал Sber Developer News:
https://t.me/SberDeveloperNews
TG-группа developers.sber.ru:
https://t.me/smartmarket_community
Habr:
https://habr.com/ru/companies/sberbank/
Благодарим за внимание! Успехов!
Описание
Учебное руководство по Platform V DataSpace на примере системы учета ГСМ (Нефтебаза)
https://platformv.sbertech.ru/products/instrumenty-razrabotchika/dataspace