Разработка модели данных#
Для разработки предметной модели может потребоваться несколько файлов. Поэтому для данных файлов необходимо предусмотреть отдельную папку. Основным файлом предметной модели является файл model.xml, который должен быть расположен в корне данной папки.
Примечание
Базовый проект можно создать из maven-архетипа. Подробная инструкция по созданию проекта из архетипа имеется в документе «Быстрый старт».
Для создания предметной модели с помощью DataSpace необходимо выполнить следующие шаги:
Создать Maven-модуль и подключить в него плагин генерации модели.
Указать путь к созданным файлам в pom.xml.
Создать связи между классами модели и необходимые агрегаты.
Создание Maven-модуля#
Для использования DataSpace в проекте необходимо создать модуль Maven и подключить в него плагин генерации модели. В файл pom.xml модуля необходимо добавить зависимость на следующий артефакт DataSpace:
<dependencies>
<dependency>
<groupId>sbp.com.sbt.dataspace</groupId>
<artifactId>common-interfaces</artifactId>
</dependency>
</dependencies>
Создание файлов модели#
Чтобы создать файлы модели, необходимо выполнить следующие действия:
Создайте директорию
modelс удобным расположением внутри проекта.Внутри директории создайте требуемые подкаталоги.
Внутри директории создайте файл модели (model.xml) и при необходимости файл статусов (status.xml):
mkdir model сd model mkdir dictionary model touch model.xml touch status.xmlДобавьте в глобальный файл pom.xml путь к ресурсам предметной модели:
<properties> <model>deposits/src/main/resources/model</model> </properties>
Управление классами#
Файл модели включает в себя XML-элементы, заданные с помощью тегов. При разметке файла модели необходимо использовать следующие правила:
Все элементы модели заключаются между парой тегов
<model>.Для типов сущностей необходимо использовать теги
<class>(открывающие и закрывающие).Для свойств классов используются теги
<property/>(допустимы самозакрывающиеся).Индексируемые свойства дублируются между тегами
<index>.
Ниже показана упрощенная иерархия тегов в модели:
<model>
<class>
<property>
<index>
Редактирование файла model.xml можно начать со следующего шаблона:
<model>
<class label="Краткое описание сущности" name="ClassName">
<property label="Краткое описание свойств" name="propertyName" type="String"/>
<index unique="false">
<property name="propertyName"/>
</index>
</class>
</model>
Создание классов#
Для создания классов выполните следующие шаги:
В файле model.xml добавьте теги модели.
Добавьте атрибуты модели. Требования к атрибутам тега см. в разделе «Модель».
Внутри тегов модели добавьте теги для каждого из классов.
Укажите название каждого из классов (атрибут
name) и его краткое описание (атрибутlabel). Требования к атрибутам см. в разделе «Класс».Добавьте текст
<property label="" name="" type=""/>для каждого из свойств класса.Введите атрибуты свойств. Требования к атрибутам классов см. в разделе «Свойство». Обязательны следующие значения:
Имя свойства: атрибут
name.Отображаемое название свойства: атрибут
label.Тип свойства: атрибут
type.
Пример файла модели: файле model-simple.xml.
Примечание
В DataSpace определен список зарезервированных слов (зарезервированные слова языка SQL поддерживаемых СУБД), которые нельзя использовать в качестве имен физических объектов. При совпадении значения атрибута
nameэлемента модели (class,propertyи т.д.) с зарезервированным словом, название целевого элемента в БД будет изменено. Например, если в model.xml определено свойство с именемclob, то в БД под это свойство будет создана колонкаCLOB_.
Добавление индекса#
Чтобы реализовать индексирование данных в базе, необходимо выполнить следующие действия:
Добавить пару тегов
<index></index>в определение класса.Ввести индексируемые параметры, как показано в примере ниже. При добавлении уникальных составных индексов необходимо учитывать их разное поведение на разных БД при наличии значений NULL в составе полей индекса. Допустим, для полей а1 и а2 класса А создан уникальный составной индекс, тогда вставка строк (а1=10,a2=NULL), (а1=10,a2=NULL) в БД Postgres не вызывает ошибку, т.к. данные строки не считаются уникальными в данной БД. А при вставке данных строк в БД Oracle будет вызвана ошибка нарушения уникальности индекса.
Обычный неуникальный индекс#
<model>
<class label="Краткое описание сущности" name="ClassName">
<property label="Краткое описание свойств" name="propertyName" type="String"/>
<index unique="false">
<property name="propertyName"/>
</index>
</class>
</model>
Составной индекс#
<model>
<class name="Coordinates" is-abstract="false" is-dictionary="false" embeddable="true">
<property name="latitude" type="STRING" length="254" mask="^(\\+|-)?(?:90(?:(?:\\.0{1,6})?)|(?:[0-9]|[1-8][0-9])(?:(?:\\.[0-9]{1,6})?))$"/>
<property name="longitude" type="STRING" length="254" mask="^(\+|-)?(?:180(?:(?:\.0{1,6})?)|(?:[0-9]|[1-9][0-9]|1[0-7][0-9])(?:(?:\.[0-9]{1,6})?))$"/>
</class>
<class name="Tank" is-abstract="false" is-dictionary="false" embeddable="false" strategy="JOINED">
<id category="MANUAL"/>
<property name="currentVal" type="BIGDECIMAL" length="38" scale="10" default-value="0">
<historical>true</historical>
</property>
<property name="location" type="Coordinates"/>
<property name="rawData" type="TEXT"/>
<property name="tankOpenDate" type="LOCALDATE">
<historical>true</historical>
</property>
<property name="tankOperationList" type="TankOperation" collection="SET" mappedBy="tank"/>
<property name="tankType" type="TankType" mandatory="true"/>
<reference name="owner" type="Customer"/>
<index unique="true">
<property name="location"/>
</index>
<index unique="false">
<property name="lastChangeDate"/>
<property name="type"/>
<property name="location.latitude"/>
<property name="owner.entityId"/>
</index>
</class>
</model>
Примечание
Обратите внимание, что в примерах составных индексов выше
можно задать индекс по всем полям embedded-сущности: порядок полей индекса соответствует порядку описания полей внутри embedded-сущности. В данном случае будет составной уникальный индекс по полям latitude и longitude;
можно явно перечислять поля, в т.ч. системные (type, lastChangeDate, objectId и др.), указывать поля внешних ссылок.
Моделирование свойств объектов#
При описании модели можно указать следующие типы свойств:
Примитивные типы (см. раздел «Использование примитивных типов»).
Специализация примитивных типов (см. раздел «Использование специализированных типов»).
Классы модели (см. раздел «Использование класса в свойстве»).
Перечисления (тип
enum) (см. раздел «Использование перечисления в свойстве»).
В дополнение к этому можно использовать значения по умолчанию.
Использование значений по умолчанию#
Для примитивных свойств, специализированных типов и перечислений можно указывать значения по умолчанию. Для этого необходимо использовать атрибут default-value:
<enum name="Size">
<value name="S"/>
<value name="M"/>
</enum>
<class name="Product">
<property name="size" type="Size" default-value="M"/>
<!-- ... -->
</class>
Помимо установки значений по умолчанию полей атрибут default-value используется еще в одном особом случае.
Если у выпущенной модели какое-нибудь поле становится обязательным для заполнения, то уже существующие записи в БД, где это поле может быть пустым, необходимо заполнить. В таком случае потребуется
указать default-value для этого поля. Значением, указанным в default-value, будут обновлены существующие записи.
Внимание
В случае, если у поля одновременно есть атрибут обязательности, то
default-valueбудет игнорироваться.При попытке выполнения транзакции с незаполненным обязательным полем будет появляться ошибка отсутствия такого поля, в том числе и при наличии атрибута
default-value.
Использование примитивных типов#
Большинство примитивных типов DataSpace схожи c типами Java: Boolean, Byte, Character (char), Short, Integer, Long, Float, Double, Date, LocalDate, LocalDateTime, OffsetDateTime, String, Text (строка неограниченной длины), BigDecimal (~java.math.BigDecimal) и Binary (~BLOB в БД Oracle).
Также предусмотрен строчный тип стандарта Юникод (Unicode String).
Подробные сведения о примитивах DataSpace и таблицу соответствия типов можно найти в разделе «Примитивные типы».
Использование специализированных типов#
В DataSpace можно использовать специализированные типы, производные от примитивов DataSpace.
Для использования специализированных типов в model.txt необходимо добавить блок <type-defs></type-defs>. Пользовательские новые типы необходимо использовать в значении поля type свойства (property) в классе (class).
Структура type-defs показана во фрагменте ниже:
<type-defs>
<type-def>
* name — имя производного типа (обязательно).
* type — оригинальный примитивный тип DataSpace (обязательно).
* length — длина (доступно для String и BigDecimal).
* scale — масштаб (доступно для BigDecimal).
Заданная длина и масштаб будет устанавливаться только в том случае, если пользователь явно не переопределил аналогичное свойство на классе:
Для типа
Stringимеется возможность задания length. По умолчанию: 254.Для типа
BigDecimalпомимоlengthвозможно задать значениеscale. По умолчанию:length = 38,scale= 10.Остальные типы не поддерживают
lengthиscale.
Пример использования для типа String и BigDecimal:
<type-defs>
<type-def length="6" name="ShortString" type="String"/>
</type-defs>
<class label="продукт" name="Product">
<property name="pinCode" type="ShortString"/> <!-- в итоге будет длина 6-->
<!--...-->
</class>
Использование класса в свойстве#
Классы можно использовать в качестве свойств других классов. Для этого имя класса необходимо указать в качестве типа свойства.
В случае использования связи OneToMany, необходимо дополнительно использовать параметр collection="set".
<class name="City" label="Город" is-dictionary="true">
<property name="code" type="String" label="Код" mandatory="true"/>
<property name="name" type="String" label="Наименование" mandatory="true"/>
<property name="offices" type="Office" label="Офисы" collection="set" mappedBy="city"/>
</class>
<class name="Office" label="Офис" is-dictionary="true">
<property name="city" type="City" label="Город" mandatory="true"/>
<property name="code" type="String" label="Код" mandatory="true"/>
<property name="name" type="String" label="Наименование" mandatory="true"/>
</class>
Использование перечисления в свойстве#
Перечисления можно использовать в качестве свойств объектов. Для этого необходимо указать имя перечисления в качества типа поля.
<enum name="Currency">
<value name="USD"/>
<value name="EUR"/>
<value name="RUB"/>
</enum>
<class name="DepositRequest" extends="Request" label="Запрос на открытие депозита">
<property name="sum" type="BigDecimal" label="Сумма"/>
<property name="currency" type="Currency" label="Валюта депозита"/>
</class>
Вычисляемые свойства#
Можно объявить свойства, вычисляемые на основе выражения, в которых могут участвовать другие не вычисляемые свойства этого же класса. Прямое изменение данных свойств не допустимо. Свойства класса в выражении могут указываться как с привязкой к классу «Client.firstName», так и без «Client.». Регистр не учитывается. Изменение выражения обратно несовместимо (в последующих версиях модели изменять не допустимо).
Вычисляемые свойства могут быть проиндексированы самостоятельно, а также входить в составные индексы.
Вычисляемые свойства возможно задавать следующими способами:
Проверяемое выражение:
<property name="c2" type="Integer"> ВЫЧИСЛЯЕМОЕ_ВЫРАЖЕНИЕ </property>Корректность вычисляемого выражения проверяется на этапе сборки. Поддерживаемые операции и функции:
унарные операции: !, NOT;
операции сравнения: =, >, <, <=, >=, !=;
логические: AND, OR;
математические операции: *, /, +, -;
функции: ABS(X), ACOS(X), ASIN(X), ATAN(X), COS(X), SIN(X), TAN(X), LOG(X,Y), SQRT(X), ROUND(X,Y), CONCAT(X,Y), LOWER(X), UPPER(Y), COALESCE(X,Y,Z,…), SUBSTRING(X,Y,Z), TRIM(X FROM Y), TRANSLATE(string, from_string, to_string);
условия: IN, NOT IN, IS NULL, IS NOT NULL, BETWEEN … AND …, NOT BETWEEN … AND …, LIKE, NOT LIKE;
условные выражения:
CASE expression CASE WHEN X1 THEN Y1 WHEN expression THEN Y1 WHEN X2 THEN Y2 WHEN expression THEN Y2 ............. ............. [ELSE Y3] [ELSE Y3] END ENDY1, Y2, Y3 — допускается expression
Непроверяемое SQL-выражение: В
sql-exprможет быть задано любое валидное для используемой БД выражение.
<property name="c1" type="Integer">
<sql-expr dbms="h2">
ВЫЧИСЛЯЕМОЕ_ВЫРАЖЕНИЕ_h2
</sql-expr>
<sql-expr dbms="oracle">
ВЫЧИСЛЯЕМОЕ_ВЫРАЖЕНИЕ_oracle
</sql-expr>
<sql-expr dbms="postgresql">
ВЫЧИСЛЯЕМОЕ_ВЫРАЖЕНИЕ_pg
</sql-expr>
</property>
<property name="c2" type="Integer">
<sql-expr dbms="h2,oracle">
ВЫЧИСЛЯЕМОЕ_ВЫРАЖЕНИЕ_h2_oracle
</sql-expr>
<sql-expr dbms="postgresql">
ВЫЧИСЛЯЕМОЕ_ВЫРАЖЕНИЕ_pg
</sql-expr>
</property>
<property name="c3" type="Integer">
<sql-expr>
ВЫЧИСЛЯЕМОЕ_ВЫРАЖЕНИЕ
</sql-expr>
</property>
Допустимые значения для dbms: h2, oracle, postgresql (возможно группировать).
Если dbms не указан, выражение будет применяться ко всем не указанным явно БД.
В <sql-expr> ... </sql-expr> свойства класса указываются: {property_name}.
Пример задания вычисляемых свойств, где c1, c2, c3, c4 — проверяемые выражения, с5 и c6 — непроверяемые SQL-выражения:
<class name="EmbeddedClass" is-abstract="false" is-dictionary="false" embeddable="true">
<property name="eProperty1" type="STRING" length="254"/>
<property name="eProperty2" type="STRING" length="254"/>
</class>
<class name="ClassA" label="класс А">
<property name="embeddedClass" type="EmbeddedClass"/>
<property name="stringField" type="String" label=""/>
<property name="integerField" type="Integer" label=""/>
<property name="byteField" type="Byte" label=""/>
<property name="code" type="String" label=""/>
<property name="c1" type="String" unique="true" >
COALESCE(stringField,'')
</property>
<property name="c2" type="Integer">
integerField+10+byteField
</property>
<property name="c3" type="string" >
CASE WHEN ClassA.stringField='Abc' and lower(stringField)!='cdf'
THEN null WHEN upper(stringField)='ABC' THEN upper(stringField)
ELSE lower(stringField) END
</property>
<property name="c4" type="String">
CASE WHEN code is not null THEN code
ELSE '' END
</property>
<property name="c5" type="Boolean">
<sql-expr dbms="oracle,h2">
CASE WHEN {stringField} ={code} TRUE THEN TRUE ELSE NULL END
</sql-expr>
<sql-expr dbms="postgresql">
CASE WHEN {stringField} =upper({embeddedClass.eProperty1}) TRUE THEN TRUE ELSE NULL END
</sql-expr>
</property>
<property name="c6" type="Boolean">
<sql-expr>
CASE WHEN {stringField} ={code} TRUE THEN TRUE ELSE NULL END
</sql-expr>
<sql-expr dbms="postgresql">
CASE WHEN {stringField} =upper({embeddedClass.eProperty1}) TRUE THEN TRUE ELSE NULL END
</sql-expr>
</property>
</class>
Пример, когда необходимо построить уникальный индекс по Имени и Фамилии без учета регистра:
<class name="Client" label="класс C">
<property name="firstName" type="String" label="Имя" mandatory="true"/>
<property name="lastName" type="String" label="Фамилия" mandatory="true"/>
<property name="firstAndLastName" type="String" unique="true" >
CONCAT(UPPER(Client.firstName),CONCAT(' ',UPPER(lastName)))
</property>
</class>
public void test() {
Packet packet1 = new Packet();
CreateClientParam createClientParam = CreateClientParam.create()
.setFirstName("Иван")
.setLastName("Иванов");
ClientRef clientRef = packet1.Client.create(createClientParam);
ClientGet clientGet = packet1.Client.get(clientRef, p -> p
.withFirstAndLastName());
assertThatCode(() -> dataspaceCorePacketClient.execute(packet1)).doesNotThrowAnyException();
// Проверка, что в вычисляемой свойства данные сформированы согласно
// заданному правилу, описанному в модели
assertEquals("ИВАН ИВАНОВ", clientGet.getFirstAndLastName());
Packet packet2 = new Packet();
packet2.Client.create(p -> p
.setFirstName("ИВАН")
.setLastName("Иванов"));
// Попытка записать в вычисляемую колонку такое же значение и получить
// срабатывание уникального индекса
assertThatCode(() -> dataspaceCorePacketClient.execute(packet2))
.hasMessageFindingMatch(".*Нарушение уникального индекса.*FIRSTANDLASTNAME.*");
}
Пример, когда необходимо проверять на уникальность поле code если поле isDeleted==false:
<class name="ClassD" label="класс D">
<property name="code" type="String" label="Коде" mandatory="true"/>
<property name="isDel" type="boolean" label="" mandatory="true"/>
<property name="codeForIndex" type="String" unique="true">
CASE
WHEN isDel=TRUE THEN NULL
ELSE code
END
</property>
</class>
public void test2() {
Packet packet1 = new Packet();
packet1.ClassD.create(p -> p
.setCode("ABC")
.setIsDel(false));
assertThatCode(() -> dataspaceCorePacketClient.execute(packet1)).doesNotThrowAnyException();
Packet packet2 = new Packet();
packet2.ClassD.create(p -> p
.setCode("ABC")
.setIsDel(true));
assertThatCode(() -> dataspaceCorePacketClient.execute(packet2)).doesNotThrowAnyException();
Packet packet3 = new Packet();
packet3.ClassD.create(p -> p
.setCode("ABC")
.setIsDel(true));
assertThatCode(() -> dataspaceCorePacketClient.execute(packet3)).doesNotThrowAnyException();
Packet packet4 = new Packet();
packet4.ClassD.create(p -> p
.setCode("ABC")
.setIsDel(false));
assertThatCode(() -> dataspaceCorePacketClient.execute(packet4))
.hasMessageFindingMatch(".*Нарушение уникального индекса.*");
}
Расширение модели#
Разрабатываемую модель можно расширить внешними файлами. Для этого необходимо выполнить следующие шаги:
В том же каталоге, где расположен файл model.xml создать каталог
import.Внутри каталога необходимо создать файл для дополнительных классов:
cd model mkdir import touch import/newClasses.xmlДобавить в созданный файл теги
<model model-name="myImportModelName">...</model>и требуемые классы:<model> <class></class> </model>
Импортируемые файлы должны оформляться аналогично model.xml, за исключением того, что в теге model нет необходимости указывать атрибуты.
Для импортируемых моделей необходимо задать идентифицирующие их имена.
По имени импортируемой модели через указание в плагине генерации SDK формируется частичные файлы работы с моделью, ограниченные именем модели.
Пример настройки плагина(см. параметр importModelName):
<plugin>
<groupId>sbp.com.sbt.dataspace</groupId>
<artifactId>model-api-generator-maven-plugin</artifactId>
<executions>
<execution>
<id>createSdk</id>
<goals>
<goal>createSdk</goal>
</goals>
<configuration>
<basePackage>${modelPackage}</basePackage>
<model>${model}</model>
<importModelName>myImportModelName</importModelName>
</configuration>
</execution>
</executions>
</plugin>
Функционал можно использовать и для простого разбиения модель на несколько xml файлов в случае ее сложности.
Если не нужно генерировать отдельные SDK по моделям — не передавайте параметр importModelName.
Для импорта будут доступны следующие элементы модели:
классы;
перечисления (enum);
интерфейсы.
Примечание
Папку для импорта можно переопределить. Для этого в основном файле model.xml необходимо прописать путь к папке, из которой будут импортироваться элементы модели:
<model ...=""> <import file="nameDir" type="IMPORT"/> ... </model>
Дробить модель рекомендуется на отдельные файлы для целых агрегатов.
Примечание
Чтобы в одной схеме базы данных использовать одну общую модель, которая будет обслуживать несколько модулей, к SDK этих модулей необходимо подключить отдельные файлы с описанием требуемых агрегатов. В этом случае разработчику SDK будет доступна только своя модель со принадлежащими ему агрегатами, и не будут доступны другие агрегаты.
Управление статусами#
Создание статусного файла#
Если поведение моделируемых объектов должно изменяться в зависимости от их состояния, необходимо реализовать статусы объектов модели. Статусы можно добавлять как в model.xml(рекомендуемый метод), так и в отдельный файл.
Для создания отдельного статусного файла необходимо выполнить следующие действия:
Создать файл status.xml в той же папке, что и файл model.xml.
Добавить инструкцию об импорте статусов в файл модели. Инструкция представляет собой следующий тег:
<import type="STATUS"/>.Добавить информацию о требуемых статусах в status.xml.
О влиянии статусов на классы модели#
При импорте статусов в модель произойдет следующее:
На объектах, указанных в теге
<status-classes>, автоматически добавятся поля для каждого описанного наблюдателя (тегstakeholder) со следующим именем:statusFor*StakeholderName.Добавится связь с таблицей истории статусов, имя свойства — statusHistory (при активации атрибута
historical).На объектах добавляется поле
statusReasonFor*StakeholderNameдля каждого описанного наблюдателя (тегstakeholder).
О сохранении истории изменения статусов#
Продукт DataSpace может сохранять историю изменения статусов.
Для этого используется атрибут historical с возможными значениями true и false. Значение атрибута автоматически наследуется всеми классами-потомками. Значение по умолчанию: false.
При необходимости сохранения информации о пользователе, внесшем изменения, можно воспользоваться функциональностью, описанной в разделе «Сохранение в историю пользователя, изменившего данные» документа «Руководство по системному администрированию».
При необходимости получения данных о переходе статуса можно воспользоваться функциональностью, описанной в разделе «Чтение истории изменения статусов» документа «Руководство прикладного разработчика».
Упрощенный пример файла модели:
<class name="A"></class>
<class extends="A" name="B"></class>
<class extends="B" name="C"></class>
<class extends="C" name="D"></class>
<class extends="D" name="E"></class>
Для этих классов указаны показанные ниже значения признака historical:
<statuses class="A"></statuses>
<statuses class="B" historical="true"></statuses>
<statuses class="D" historical="false"></statuses>
<statuses class="E" historical="true"></statuses>
На схеме показано, какие классы модели будут сохранять историю изменения статусов:

Пример получения истории статусов через поисковое API#
Возьмем некоторый продукт клиента:
<class name="ProductParty">
<property name="code" type="String" label="код"/>
...
</class>
Для продукта определены статусы с сохранением истории:
<statuses class="ProductParty" historical="true">
<stakeholder-link code="platform">
<status code="productCreated" description="Начальный статус продукта" name="Создание продукта"
initial="true">
<to status="productClosed"/>
<to status="productCheck"/>
</status>
<status code="productClosed" name="На закрытие"/>
<status code="productCheck" name="На рассмотрении">
<to status="productClosed"/>
</status>
</stakeholder-link>
<stakeholder-link code="service">
<status code="depositOpened" description="Начальный статус депозита ЮЛ" name="Начало открытия депозита"
initial="true">
<to status="depositCheck"/>
<to status="depositClosed"/>
</status>
<status code="depositCheck" name="На рассмотрении">
<to status="depositClosed"/>
</status>
<status code="depositClosed" name="Закрытие депозита"/>
</stakeholder-link>
<stakeholder-link code="serviceProductWatcher">
<status code="productCreatedProductWatcher" description="Начальный статус продукта"
name="Создание продукта" initial="true">
<to status="productClosedProductWatcher"/>
</status>
<status code="productClosedProductWatcher" name="На рассмотрении"/>
</stakeholder-link>
</statuses>
Создадим и проведем объект по жизненному циклу:
Packet createPacket = Packet.createPacket();
ProductPartyRef productPartyRef = createPacket.productParty.create(param -> param.setCode(myCode));
executePacket(createPacket);
Packet updatePacket1 = Packet.createPacket();
updatePacket1.productParty.update(productPartyRef, param ->
param.setStatusForPlatform(ProductPartyPlatformStatus.PRODUCTCHECK, "Передан на проверку УФК"));
executePacket(updatePacket1);
Packet updatePacket2 = Packet.createPacket();
updatePacket2.productParty.update(productPartyRef, param ->
param.setStatusForPlatform(ProductPartyPlatformStatus.PRODUCTCLOSED, "Клиент закрыл продукт"));
executePacket(updatePacket2);
При создании не указываются никакие статусы — значения статусов получат значения инициализирующих (initial=»true») статусов. Затем обновим статус для наблюдателя platform с указанием причины изменения до productCheck, а затем еще раз — до productClosed.
Для того чтобы получить историю изменения жизненного цикла этого продукта можно написать следующий код (запрос получения истории изменения жизненного цикла задается указанием withStatusHistory, в котором можно описать детали возвращаемой информации):
GraphCollection<ProductPartyGet> productPartyGets = dataspaceCoreSearchClient().searchProductParty(product ->
product.withName()
.withStatusHistory(history -> history.withStatus(StatusWith::withCode).withChangeReason().withChangeTime()
.setWhere(it -> it.status().stakeholder().codeEq("platform")))
.setWhere(it -> it.codeEq(myCode)));
Assertions.assertEquals(1, productPartyGets.size());
ProductPartyGet productPartyGet = productPartyGets.get(0);
productPartyGet.getStatusHistory().forEach(it ->
System.out.println("Статус: " + it.getStatus().getCode() +
", Причина: " + it.getChangeReason() +
", Время изменения:" + it.getChangeTime()));
Мы отсекли жизненный цикл по не интересным нам наблюдателям service и serviceProductWatcher, ограничившись лишь platform.
Результат вывода будет следующим:
Статус: productCreated, причина: Установлен по умолчанию, Время изменения:Tue Nov 16 17:50:12 MSK 2021
Статус: productCheck, причина: Передан на проверку УФК, Время изменения:Tue Nov 16 17:50:13 MSK 2021
Статус: productClosed, причина: Клиент закрыл продукт, Время изменения:Tue Nov 16 17:50:13 MSK 2021
О наследовании статусов#
Наследование классов модели и назначенные классам статусы отражены в таблице ниже под примерами кода. Некоторые атрибуты удалены для лучшего понимания идеи процесса.
Пример реализации наследования в модели:
<class name="Product">
<property name="series" type="Date"/>
</class>
<class extends="Product" name="ProductExt">
<property name="name" type="String"/>
</class>
<class extends="ProductExt" name="ProductExt2">
<property name="code" type="String"/>
</class>
<class extends="ProductExt2" name="ProductExt3">
<property name="kind" type="String"/>
</class>
Описание заголовка статусов:
<status-classes class="Product">
<stakeholder code="platform"/>
</status-classes>
<status-classes class="ProductExt">
<stakeholder code="service"/>
</status-classes>
<status-classes class="ProductExt2">
<stakeholder code="party"/>
</status-classes>
Внимание!
Наблюдатели, указанные в теге
stakeholder, в процессе генерации станут новыми свойствами на сущностях. Таким образом, нет необходимости повторно определять наблюдателя на потомке внутри status-classes.
Далее рассмотрены следующие примеры:
Назначение каждому классу своего наблюдателя с переносом статусов потомку.
Переопределение статусов наблюдателя предка (по модели) в потомке.
Назначение классу наблюдателя с переносом статусов потомку#
Во фрагменте кода приведен пример содержимого файла status.xml для рассматриваемой ситуации:
<statuses class="Product">
<stakeholder-link code="platform">
<status code="productPl"/>
</stakeholder-link>
</statuses>
<statuses class="ProductExt">
<stakeholder-link code="service">
<status code="productExtSr"/>
</stakeholder-link>
</statuses>
<statuses class="ProductExt2">
<stakeholder-link code="party">
<status code="productExt2Pa"/>
</stakeholder-link>
</statuses>
В таблице показано, какие статусы из примера выше будут доступны наблюдателям с учетом порядка наследования классов:
Тип объекта, наблюдатель |
platform |
service |
party |
|---|---|---|---|
Product |
ProductPI |
— |
— |
ProductExt |
ProductPI |
productExtSr |
— |
ProductExt2 |
ProductPI |
productExtSr |
productExt2Pa |
ProductExt3 |
ProductPI |
productExtSr |
productExt2Pa |
ProductExt3 — класс, который автоматически получает статусы от предков.
Для данного примера важно, что каждый класс-потомок получает от предка статусы по наблюдателям.
Переопределение статусов наблюдателя предка в потомке#
Во фрагменте кода приведен пример содержимого файла status.xml для рассматриваемой ситуации:
<statuses class="Product">
<stakeholder-link code="platform">
<status code="productPl"/>
</stakeholder-link>
</statuses>
<statuses class="ProductExt">
<stakeholder-link code="service">
<status code="productSr"/>
</stakeholder-link>
</statuses>
<statuses class="ProductExt2">
<stakeholder-link code="platform">
<status code="productExt2tPl"/>
</stakeholder-link>
<stakeholder-link code="party">
<status code="productExt2Pa"/>
</stakeholder-link>
</statuses>
<statuses class="ProductExt3">
<stakeholder-link code="service">
<status code="productExt3Sr"/>
</stakeholder-link>
</statuses>
В таблице показано, какие статусы из вышеприведенного примера будут доступны наблюдателям:
Тип объекта\наблюдатель |
platform |
service |
party |
|---|---|---|---|
Product |
productPl |
* |
* |
ProductExt |
productPl |
productSr |
* |
ProductExt2 |
productExt2tPl |
productSr |
productExt2Pa |
ProductExt3 |
productExt2tPl |
productExt3Sr |
productExt2Pa |
Если переопределяемый наблюдатель в наследуемом классе определяет свои статусы, то статусы предков становятся недостижимыми.
Конкурентное изменение статуса#
Если не использовать явно прикладную блокировку объекта, то выполняющиеся параллельно пакеты команд при определенных условиях могут перевести объект в статус, в который при последовательном изменении переход был бы невозможен. Пример:
<stakeholder-link code="someStakeholder">
<status code="new" initial="true">
<to status="done"/>
<to status="rejected"/>
</status>
<status code="done"/>
<status code="rejected"/>
</stakeholder-link>
Конкурентное обновление сущности из статуса new в статусы done и rejected без блокировки может выполниться для нескольких процессов без выброса исключения. При этом итоговый статус может оказаться любым из done и rejected.
Начиная с 1.16 версии добавлена конкурентная проверка изменения статуса. Если статус был обновлен другим процессом, то будет выброшено исключение вида:
Ошибка обработки команды id = '0', name = 'update': Статусы обновлены другим процессом.
Проверку можно отключить, задав настройку dataspace-core.status-move-update-control=false(по умолчанию значение настройки true).
Изменения статусной модели#
Разрешено добавлять новые статусы и произвольным образом менять граф переходов между статусами. Разрешено также удалять статусы из модели, однако для сохранения обратной совместимости и ссылочной целостности они фактически в DataSpace остаются, но будут помечены в SDK как «устаревшие».
Отключение проверки переходов#
Чтобы отключить контроль за переходами статусов, наблюдателю необходимо удалить теги <to status=".."/> внутри статусов. Указание начального статуса в этом случае становится обязательным к заполнению (initial).
Примечание
Наличие хотя бы одного перехода автоматически закрывает возможность произвольного движения статусов для конкретного наблюдателя.
Имеется возможность отключить контроль за разрешенными переходами статусной модели для конкретного запроса - см. раздел отключить контроль за переходами документа «GraphQL протокол»
Устаревшие переходы в SDK#
Устаревшие переходы не отображаются в SDK.
Поведение регулируется параметром плагина disableRemovedTransitions.
По умолчанию значение параметра true.
Указание длины поля statusReasonFor*StakholderName#
По умолчанию длина поля равна 254. При необходимости значение можно изменить, указав в теге stakeholder-link атрибут reason-length.
Правила изменения те же, что и для обычного поля.
Пример:
<stakeholder-link code="platform" reason-length="512">
<status code="active" name="active" initial="true">
<to status="close"/>
</status>
<status code="close" name="close">
<to status="active"/>
</status>
</stakeholder-link>
Конкурентная работа со статусными полями#
Внимание!
Статусы не имеют защиты от конкурентного использования. То есть при параллельном использовании статусов в одном объекте может возникнуть ситуация потери целостности данных. Для исключения такой ситуации следует использовать оптимистические или пессимистические блокировки (прикладная блокировка, чтение с блокировкой, проверка версии агрегата, проверка ожидаемого значения
compare).
Логика смены статуса#
Если задана логика изменения статусов, т.е. определены переходы, то при изменении статуса проверяется возможность перехода из текущего статуса в целевой.
В случае, если переход не допустим, выбросится исключение Не объявлен переход из статуса <текущий> в статус <целевой>.
В случае, если код текущего статуса совпадает с целевым, то переход игнорируется.
Внимание!
С версии 1.14 при определении необходимости перехода сравнивается не только код статуса, но и причина (reason) перехода. В случае несовпадения кода или причины определяется, что происходит изменение жизненного цикла объекта. Таким образом, для возможности изменения только причины (reason) без изменения кода необходимо определить переход в текущий статус. В противном случае возникнет исключение недопустимости перехода.
Задание связей между классами#
В DataSpace предусмотрены следующие способы задания связей между классами:
Наследование классов#
Модель поддерживает наследование классов. Наследование реализуется за счет указания атрибута extends на классе-потомке.
<class name="Product">
</class>
<class name="Deposit" extends="Product">
</class>
<class name="LegalPersonDeposit" extends="Deposit">
</class>
Наследование с использованием абстрактных классов#
Внимание!
Наследовать можно и от абстрактных классов, но ссылки на абстрактные классы (например,
<property type="*AbstractClass*"/>) недопустимы. Ограничения связаны с невозможностью определения конкретной сущности на физическом уровне (абстрактный класс не является Entity и не имеет таблицы).
Таблица содержит возможные схемы наследования сущностей:
Без абстрактных классов |
Наследование от базового абстрактного класса |
Промежуточный абстрактный класс |
|---|---|---|
|
|
|
Пример кода для модели без абстрактных классов класса:
<class name="Product">
</class>
<class name="Deposit" extends="Product">
</class>
<class name="LegalPersonDeposit" extends="Deposit">
</class>
Пример наследования от базового абстрактного класса:
<class name="AbstractProduct" is-abstract="true">
</class>
<class name="Product" extends="AbstractProduct">
</class>
<class name="Deposit" extends="Product">
</class>
Пример промежуточного абстрактного класса:
<class name="Product">
</class>
<class name="AbstractDeposit" extends="Product" is-abstract="true">
</class>
<class name="Deposit" extends="AbstractDeposit">
</class>
Стратегии наследования#
Стратегия наследования описывается на первом неабстрактном классе иерархии наследования (то есть на классе, у которого либо предки отсутствуют, либо все они являются абстрактными классами). Стратегию нельзя переопределять на классах-потомках. Данный атрибут влияет на физическое представление объектов в БД. Возможны следующие стратегии:
JOINED (по умолчанию).
Множественность связей#
В DataSpace поддерживаются следующие множественные связи между классами:
ManyToOne (многие к одному);
OneToMany (один ко многим);
OneToOne (один к одному).
Для создания связи ManyToMany (многие к многим) необходимо создать промежуточные сущности.
Связь OneToMany#
Связь является двунаправленной. Физически ссылка хранится на неколлекционном объекте.

В терминах XML это может выглядеть следующим образом. Для рабочего варианта блока модели сущности должны быть частью одного агрегата. Дополнительные справочные сведения можно найти в разделе «DDD-агрегаты».
<class name="Product">
<property collection="set" mappedby="product" name="clients" type="Client"/>
</class>
<class name="Client">
<property name="product" type="Product"/>
</class>
Связь ManyToOne как ссылка#
Связь является однонаправленной.

В терминах XML это выглядит следующим образом (для рабочего варианта блока модели сущности должны быть частью одного агрегата):
<class name="Product">
<property name="client" type="Client"/>
</class>
<class name="Client">
<property name="name" type="String"/>
</class>
Связь ManyToOne в связи с коллекцией#
Связь является двунаправленной. Физически ссылка хранится на неколлекционном объекте.

В терминах XML это выглядит следующим образом (для рабочего варианта блока модели сущности должны быть частью одного агрегата):
<class name="Product">
<property name="client" type="Client"/>
</class>
<class name="Client">
<property collection="set" mappedby="client" name="products" type="Product"/>
</class>
Связь OneToOne как уникальная ссылка#
Связь является однонаправленной.

В терминах XML это выглядит следующим образом:
<class name="Product">
<property name="client" type="Client" unique="true"/>
</class>
<class name="Client">
<property name="name" type="String"/>
</class>
Связь ManyToMany через сущность в модели#
Для создания связи ManyToMany необходимо создать промежуточный класс.
На диаграмме ниже между классами Product и Client создан промежуточный класс — ProductClientLink.
От Product к промежуточной сущности образована связь OneToMany, в обратную сторону — ManyToOne.
Промежуточный класс использует внешнюю ссылку к классу Client.

В терминах XML такая связь может выглядеть следующим образом:
<class name="Product">
<property name="clients" type="ProductClientLink" collection="set" mappedBy="product"/>
</class>
<class name="ProductClientLink">
<property name="product" type="Product" parent="true"/>
<reference name="client" type="Client"/> <!-- Создается внешняя ссылка, см. тему "Внешние ссылки" -->
</class>
<class name="Client"> <property name="name" type="String"/>
</class>
Embedded-классы#
При проектировании модели имеется возможность определить так называемые embedded-классы — классы, определяющие набор примитивных или enum-полей.
Ссылки и коллекционные атрибуты в embedded-классах запрещены.
Сам по себе embedded-класс (размеченный в модели данных свойством embeddable="true") не является отдельным объектом, но может выступать в качестве атрибута класса, не являющегося embedded.
В примере ниже представлен синтаксис определения таких классов и их использования в качестве атрибутов:
<class name="Address" label="Книга" embeddable="true">
<property name="city" type="String" label="Город"/>
<property name="street" type="String" label="Улица"/>
<property name="houseNumber" type="String" label="Номер дома"/>
</class>
<class name="BookStore" label="Книжный магазин">
<property name="name" type="String" label="Название"/>
<property name="addressReg" type="Address" label="Адрес регистрации"/>
<property name="addressFact" type="Address" label="Адрес фактический"/>
</class>
Управление пользовательскими SQL-запросами#
Пользовательские SQL-запросы необходимо воспринимать как параметризованное представление. В результате объявления пользовательского SQL-запроса будут созданы сущности, аналогичные классам, с возможностью применения к ним только поисковых методов.
Для объявления пользовательского SQL-запроса необходимо в файле модели model.xml внутри тега
<query name="Query1" label="searchPerformedServiceByCodeAndName" description="Поиск сервисов по коду и имени">
<params>
<param name="template" type="String" collection="true" label="" description=""/>
<param name="name" type="String" default-value="abc"/>
</params>
<implementations>
<sql dbms="h2">select t1.code code, t1.name name, t1.OBJECT_ID id from ${dspc.schemaPrefix}T_PERFORMEDSERVICE t1 where t1.code in (${template}) AND t1.name = ${name}</sql>
<sql dbms="postgresql">select t1.code code, t1.name name, t1.OBJECT_ID id from ${dspc.schemaPrefix}T_PERFORMEDSERVICE t1 where t1.code in (${template}) AND t1.name = ${name}</sql>
<sql dbms="oracle">select t1.code code, t1.name name, t1.OBJECT_ID id from ${dspc.schemaPrefix}T_PERFORMEDSERVICE t1 where t1.code in (${template}) AND t1.name = ${name}</sql>
</implementations>
<id name="id" label="id label" description="id description"/>
<property name="code" type="String" label="" description=""/>
<property name="name" type="String"/>
</query>
Примечание:
query— тег, открывающий объявление пользовательского SQL-запроса:name— атрибут, задающий имя запроса. С учетом данного имени будут созданы поисковые классы (включая классы для работы с результатом поиска) (обязательный атрибут).label— метка запроса, метаинформация, в алгоритмах не используется (необязательный атрибут).description— текстовое описание запроса, в алгоритмах не используется (необязательный атрибут).
params— тег, открывающий описание параметров, применяемых в запросе (необязательный раздел).param— тег, описывающий отдельный параметр запроса:name— атрибут, в котором указывается имя параметра в запросе (обязательный атрибут). Подстановка параметра в запрос осуществляется конструкцией вида:${paramName}.type— указывается тип параметра (параметры могут иметь только примитивные типы) (обязательный атрибут).collection— признак коллекционного параметра, допустимые значения: «true»/»false». Значение по умолчанию — «false» (необязательный атрибут).default-value— значение параметра по умолчанию, данное значение используется, если значение параметра не было передано (необязательный атрибут).label— метка параметра, метаинформация, в алгоритмах не используется (необязательный атрибут).description— текстовое описание параметра, в алгоритмах не используется (необязательный атрибут).
implementations— тег, открывающий раздел описания SQL-запроса (описаний запроса может быть несколько, если используется несколько БД с разными диалектами) (обязательный раздел).sql— тег, описывающий SQL-запрос в нативном для БД синтаксисе:dbms— атрибут указывающий диалект запроса. Допустимые значения: «h2», «postgresql», «oracle» (необязательный атрибут). Когда он задан, SQL-запрос будет выполняться только на БД, соответствующих указанному диалекту. Отсутствие атрибута означает, что SQL-запрос будет выполняться на тех БД, для которых явно не задан SQL-запрос с подходящим диалектом. Атрибут может содержать несколько значений, указанных через запятую, например: «h2,postgresql».
id— тег, задающий имя поля, содержащего идентификатор записи (всегда имеет тип String).name— атрибут в котором указывается имя поля результата, содержащее идентификатор записи (обязательный атрибут).label— метка, метаинформация, в алгоритмах не используется (необязательный атрибут).description— текстовое описание, в алгоритмах не используется (необязательный атрибут).
property— тег, описывающий отдельное поле результата:name— имя поля результата. Должно соответствовать имени поля в запросе.type— тип поля результата. Допускаются только примитивные типы.label— метка, метаинформация, в алгоритмах не используется (необязательный атрибут).description— текстовое описание, в алгоритмах не используется (необязательный атрибут).
Ограничения#
Пользовательские SQL-запросы не поддерживают наследование — нельзя унаследовать один запрос от другого.
Параметры и поля результата запроса могут быть только примитивных типов.
Пользовательский SQL-запрос не предоставляет возможности организации вложенного получения (запроса) связанных сущностей. При этом необходимые поля вложенной сущности могут быть получены путем указания (запроса) в самом SQL-запросе.
Указание имен таблиц в пользовательских запросах#
DataSpace требует перед всеми именами таблиц и представлений, использованных в пользовательском SQL-запросе, добавить параметр ${dspc.schemaPrefix}.
Этот параметр не нужно указывать в секции <params>, значение подставится автоматически при исполнении запроса из настроек dataspace-core.
Параметр ${dspc.schemaPrefix} необходимо добавлять непосредственно перед именем таблицы без пробелов и точек
(при наличии имени схемы точка будет подставлена автоматически).
Пример:
<sql>select t1.object_id id, t1.code, t2.name, t2.object_id child_id from ${dspc.schemaPrefix}t_aggroot t1 left join ${dspc.schemaPrefix}t_aggchild t2 on t2.aggroot = t1.object_id where t1.code in ${template}</sql>
Примеры запросов для контроля очереди событий модуля подписок#
Запрос на получение очереди событий подписок с ограничением по статусам элементов очереди.
<query name="EventQueueItemQuery" label="searchEventQueueItem" description="Поиск элементов очереди обработки событий указанных статусов модуля подписок">
<params>
<param name="subscriptionId" type="String" default-value="" label="subscriptionId" description="ID подписки"/>
<param name="eventType" type="String" default-value="" label="eventType" description="Тип события"/>
<param name="eventId" type="String" default-value="" label="eventId" description="ID события"/>
<param name="aggregateRootType" type="String" default-value="" label="aggregateRootType" description="Тип корня агрегата"/>
<param name="aggregateRootId" type="String" default-value="" label="aggregateRootId" description="ID корня агрегата"/>
<param name="statuses" type="String" collection="true" label="statuses" description="Статусы"/>
<param name="from" type="OffsetDateTime" default-value="-999999999-01-01T00:00:00.000Z" label="from" description="Начальная временная метка"/>
<param name="until" type="OffsetDateTime" default-value="+999999999-12-31T23:59:59.999Z" label="until" description="Конечная временная метка"/>
</params>
<implementations>
<sql>
select
concat(tdseqi.objectid_subscriptionid, tdseqi.objectid_eventid) id,
tdseqi.objectid_subscriptionid subscriptionId,
tdseqi.eventtype eventType,
tdseqi.objectid_eventid eventId,
tdseqi.aggregateroottype aggregateRootType,
tdseqi.aggregaterootid aggregateRootId,
tdseqi.status status,
tdseqi.creationtimestamp creationTimestamp
from ${dspc.schemaPrefix}t_dspc_sys_event_queue_item tdseqi
where
(tdseqi.status in (${statuses})) and
(${subscriptionId} is null or ${subscriptionId} = '' or tdseqi.objectid_subscriptionid = ${subscriptionId}) and
(${eventType} is null or ${eventType} = '' or tdseqi.eventtype = ${eventType}) and
(${eventId} is null or ${eventId} = '' or tdseqi.objectid_eventid = ${eventId}) and
(${aggregateRootType} is null or ${aggregateRootType} = '' or tdseqi.aggregateroottype like concat('%', ${aggregateRootType})) and
(tdseqi.creationtimestamp >= ${from}) and
(tdseqi.creationtimestamp <= ${until})
order by tdseqi.creationtimestamp desc
</sql>
</implementations>
<id name="id" label="ID" description="ID элемента очереди (ID подписки + ID события)"/>
<property name="subscriptionId" type="String" label="subscriptionId" description="ID подписки"/>
<property name="eventType" type="String" label="eventType" description="Тип события"/>
<property name="eventId" type="String" label="eventId" description="ID события"/>
<property name="aggregateRootType" type="String" label="aggregateRootType" description="Тип корня агрегата"/>
<property name="aggregateRootId" type="String" label="aggregateRootId" description="ID корня агрегата"/>
<property name="status" type="String" label="status" description="Статус"/>
<property name="creationTimestamp" type="OffsetDateTime" label="creationTimestamp" description="Временная метка создания элемента очереди"/>
</query>
Запрос аналогичный предыдущему, но без ограничения по статусам.
Примечание
Для списочных полей нет возможности предусмотреть пустое или неопределенное значение (ограничение языка sql). Если требуется запрос без передачи списка, то такой запрос выделяется в отдельный пользовательский sql запрос.
<query name="AllStatusesEventQueueItemQuery" label="searchAllStatusesEventQueueItem" description="Поиск элементов очереди обработки событий всех статусов модуля подписок">
<params>
<param name="subscriptionId" type="String" default-value="" label="subscriptionId" description="ID подписки"/>
<param name="eventType" type="String" default-value="" label="eventType" description="Тип события"/>
<param name="eventId" type="String" default-value="" label="eventId" description="ID события"/>
<param name="aggregateRootType" type="String" default-value="" label="aggregateRootType" description="Тип корня агрегата"/>
<param name="aggregateRootId" type="String" default-value="" label="aggregateRootId" description="ID корня агрегата"/>
<param name="from" type="OffsetDateTime" default-value="1970-01-01T00:00:00Z" label="from" description="Начальная временная метка"/>
<param name="until" type="OffsetDateTime" default-value="2170-01-01T00:00:00Z" label="until" description="Конечная временная метка"/>
</params>
<implementations>
<sql>
select
concat(tdseqi.objectid_subscriptionid, tdseqi.objectid_eventid) id,
tdseqi.objectid_subscriptionid subscriptionId,
tdseqi.eventtype eventType,
tdseqi.objectid_eventid eventId,
tdseqi.aggregateroottype aggregateRootType,
tdseqi.aggregaterootid aggregateRootId,
tdseqi.status status,
tdseqi.creationtimestamp creationTimestamp
from ${dspc.schemaPrefix}t_dspc_sys_event_queue_item tdseqi
where
(${subscriptionId} is null or ${subscriptionId} = '' or tdseqi.objectid_subscriptionid = ${subscriptionId}) and
(${eventType} is null or ${eventType} = '' or tdseqi.eventtype = ${eventType}) and
(${eventId} is null or ${eventId} = '' or tdseqi.objectid_eventid = ${eventId}) and
(${aggregateRootType} is null or ${aggregateRootType} = '' or tdseqi.aggregateroottype like concat('%', ${aggregateRootType})) and
(${aggregateRootId} is null or ${aggregateRootId} = '' or tdseqi.aggregaterootid = ${aggregateRootId}) and
(tdseqi.creationtimestamp >= ${from}) and
(tdseqi.creationtimestamp <= ${until})
</sql>
</implementations>
<id name="id" label="ID" description="ID элемента очереди (ID подписки + ID события)"/>
<property name="subscriptionId" type="String" label="subscriptionId" description="ID подписки"/>
<property name="eventType" type="String" label="eventType" description="Тип события"/>
<property name="eventId" type="String" label="eventId" description="ID события"/>
<property name="aggregateRootType" type="String" label="aggregateRootType" description="Тип корня агрегата"/>
<property name="aggregateRootId" type="String" label="aggregateRootId" description="ID корня агрегата"/>
<property name="status" type="String" label="status" description="Статус"/>
<property name="creationTimestamp" type="OffsetDateTime" label="creationTimestamp" description="Временная метка создания элемента очереди"/>
</query>
Использование DDD-агрегатов#
Деление модели на DDD-агрегаты — принцип построения деревьев модели, в каждом из которых данные сгруппированы вокруг центрального класса (корня). DataSpace поддерживает использование DDD-агрегатов.
Примечание
Связи между агрегатами отличаются от связей между простыми объектами использованием параметра
parent="true".
Для связей OneToMany и ManyToOne необходимо указать связанную сущность как параметр. В дополнение к этому необходимо указать множественную связь: collection="set" mappedBy="{имяОбратнойСсылки}".
<class name="Product">
<property name="client" type="Client" parent="true"/>
</class>
<class name="Client">
<property name="products" type="Product" collection="set" mappedBy="client"/>
</class>
Для связи OneToOne необходимо указать связанную сущность как параметр. Один из параметров необходимо указать, как родительский (parent="true"), для второго необходимо указать обратную ссылку (mappedBy="client").
<class name="Product">
<property name="client" type="Client" parent="true"/>
</class>
<class name="Client">
<property name="product" type="Product"
mappedBy="client"/>
</class>
Дополнительные сведения о способах использования DDD-агрегатов можно найти в справочном разделе «DDD-агрегаты».
Историцирование#
Предназначено для ведения истории изменения заданных атрибутов сущностей.
При работе функциональности имеются коллизии, которые необходимо учитывать. С информацией по коллизиям можно ознакомиться в документе «Руководство прикладного разработчика» в разделе «Коллизии историцирования».
При необходимости сохранения информации о пользователе, внесшем изменения, см. описание данной функциональности в разделе «Сохранение в историю пользователя, изменившего данные» документа «Руководство по системному администрированию».
Минимальной единицей историцирования является атрибут сущности модели.
Для того чтобы пометить поле, как историцируемое, необходимо на модели добавить полю атрибут historical="true".
Внимание!
Нельзя историцировать поля
clobиblob, поля объектов embedded, поля обратных ссылок, а также поля-коллекции.
Внимание!
В плагин генерации модели и SDK (<artifactId>model-api-generator-maven-plugin</artifactId>) необходимо добавить настройку <enableHistoryGenerators>true</enableHistoryGenerators> (для целей: createSdk и createModel). В артефакте с моделью и SDK должна быть подключена зависимость sbp.com.sbt.dataspace:historical-changes-interfaces.
Внимание!
Ссылочные поля историцируются, как примитивные, т.е. историцируется само значение ссылки (идентификатор), а не расположенный по ней объект.
Пример класса с историцируемыми полями:
<class name="Product">
...
// историцруемое поле
<property name="code" type="String" historical="true"/>
// историцруемое поле
<property name="name" type="String" historical="true"/>
// не историцируемое поле
<property name="description" type="String"/>
// историцируемая ссылка
<property name="mainService" type="Service" historical="true"/>
</class>
<class name="Service">
...
</class>
Если на классе объявлены историцируемые поля, то для всех классов в цепочке наследования (как вверх, так и вниз) создаются классы историцирования. Имя класса историцирования формируется из имени класса с постфиксом History. Например, для класса Product имя класса историцирования будет ProductHistory.
Для всех классов в рамках одной цепочки наследования формируется одна таблица в БД для хранения исторических данных. Название таблицы формируется из префикса t_, имени первого неабстрактного класса в цепочке наследования и суффикса history. Для первого неабстрактного класса Product имя таблицы с историческими данными будет выглядеть следующим образом — t_producthistory.
Получение данных историцирования и детали работы механизма описаны в документе «Руководство прикладного разработчика» в разделе «Историцирование (получение данных)».
Разметка элементов модели данных тегами#
Для элементов модели данных предметной области предусмотрена возможность разметки пользовательскими тегами. Теги перечисляются через запятую в атрибуте tags на таких элементах, как class, event, property, reference.
Пример разметки элементов модели данных тегами.
<!--<model model-name="qs1_15" version="DEV-SNAPSHOT" xmlns="DataspaceModel">-->
<model model-name="product_sales">
<class name="Product" tags="Общедоступно">
<property name="code" type="String"/>
<property name="description" type="Text"/>
<property name="tariff" type="Decimal"/>
<property name="discountGroup" type="DiscountGroup" tags="ДСП"/>
</class>
<class name="Sale" tags="ДСП">
<property name="saleReference" type="String"/>
<reference name="product" type="Product" collection="set" tags="Конфиденциально"/>
<property name="customer" type="Customer" tags="Конфиденциально"/>
<property name="saleAmount" type="Decimal" tags="Коммерческая_тайна"/>
</class>
<class name="Customer" embeddable="true">
<property name="phone" type="String" tags="ПДн"/>
<property name="firstName" type="String" tags="ПДн"/>
<property name="lastName" type="String" tags="ПДн"/>
<property name="email" type="String" tags="ПДн"/>
<property name="deliveryAddress" type="String" tags="ДСП"/>
<property name="keyword" type="String" tags="ПДн, Секретно"/>
</class>
<event name="SaleChangeEvent" extends="BaseSnapshotEvent" tags="Конфиденциально">
<property name="sale" type="Sale" parent="true"/>
<parents-property name="saleAmount"/>
</event>
<enum name="DiscountGroup">
<value name="VIP"/>
<value name="PREMIUM"/>
<value name="PROMOTION"/>
</enum>
</model>
В этом примере теги использованы для разметки элементов модели данных признаками конфиденциальности:
сущность
Productотнесена к категории общедоступных данных с помощьюtags="Общедоступно";сущность
Sale, свойствоdiscountGroupсущностиProductи свойствоdeliveryAddressembeddable-классаCustomerотнесены к категории «Для служебного пользования» за счет разметкиtags="ДСП";свойства
productиcustomerсущностиSaleотнесены к категории «Конфиденциально» с помощьюtags="Конфиденциально";свойство
saleAmountсущностиSaleотнесено к категории «Коммерческая тайна» за счет разметкиtags="Коммерческая_тайна";свойства
phone,firstName,lastName,email,keywordembeddable-классаCustomerразмечены категорией «Персональные данные» путем разметкиtags="ПДн";а свойство
keyword, кроме того, отнесено и к секретным данным добавлением тега"Секретно".
Имена тегов определяются разработчиком модели данных предметной области.
К тегам предъявляются требования:
длина имени тега не превышает 63 символов;
имена тегов могут содержать алфавитно-цифровые символы unicode, а также спецсимволы: подчеркивание „_“, точка „.“, дефис „-„;
имена тегов чувствительны к регистру;
теги в списке разделяются запятыми;
regexp whitespace на границах имен игнорируется (символы, входящие в набор
[ \t\n\r\f\v]).
Примечание
Для embeddable и abstract классов указание tags не предусмотрено, поскольку эти классы сами не имеют физической реализации. При этом для элементов property и reference в составе embeddable и abstract классов указывать tags можно. tags на классах не наследуются - каждый класс-потомок должен заново объявить свои теги. tags на свойствах наследуются, в том числе от абстрактных классов.
Примечание
Для свойств, создаваемых в event через parents-property, tags будут скопированы с исходного свойства.
Применение тегов#
Разметка тегами может использоваться для отбора элементов модели данных для разных целей, в том числе для отношения с программными функциями DataSpace. В частности, теги могут применяться для отбора сущностей и свойств для включения в так называемые «черные» и «белые» списки для фильтрации передаваемых данных при репликации в хранилище данных.
Для этого в модели данных предметной области вводится элемент feature-tags, в котором при помощи элементов feature задается связь программной функции (feature) с разметкой тегами.
Имена (name) feature в составе feature-tags должны быть уникальным и входить в список допустимых для текущей версии DataSpace имен: arch-blacklist, arch-whitelist.
Пример задания отношения для формирования «черных» списков (feature name arch-blacklist) и «белых» списков (feature name arch-whitelist):
<model model-name="product_sales">\
...
<feature-tags>
<feature name="arch-blacklist" tags="ПДн, Конфиденциально, Коммерческая_тайна, Секретно"/>
<feature name="arch-whitelist" tags="ДСП, Общедоступно"/>
</feature-tags>
</model>
В этом примере разметка выполняется для включения в набор данных для репликации сущностей и свойств, маркированных тегами ДСП и Общедоступно,
за исключением запрещенных к репликации сущностей и свойств, размеченных тегами ПДн, Конфиденциально, Коммерчекая_тайна, Секретно.
Формирование «белых» и «черных» списков для репликации в хранилище данных#
При запуске цели generateMeta плагина model-api-generator-maven-plugin в папке рядом с ldm-файлом по выполненной как в приведенных примерах разметке сформируются файлы, содержащие элементы «белого» и «черного» списков с именами whitelist.txt и blacklist.txt соответственно.
blacklist.txt:
MyFeatureClass:*
MyFeatureClass:code
MyFeatureClass:code2Tags
whitelist.txt:
MyFeatureClass:code2Tags
MyFeatureClass:reference
MyFeatureClass:referenceCollection
MyFeatureClass:myFeatureEmbedded
Примечание
События (event) не подлежат репликации, поэтому их разметка тегами не влияет на фильтрацию.


