Skip to main content

Провайдеры данных

В N2O Framework визуальные компоненты связываются с данными через объекты и выборки. Объекты и выборки делегируют свои вызовы провайдерам данных.

Провайдеры — это универсальный способ обращения к источнику или к сервису предоставляющему данные. N2O поддерживает провайдеры SQL, REST, GraphQl, Spring Beans, EJB, MongoDB и другие.

Объект

Объект — это сущность предметной области. Он объединяет в себе все операции над этой сущностью и её валидации.

Объекты создаются с помощью файлов [id].object.xml.

<?xml version='1.0' encoding='UTF-8'?>
<object xmlns="http://n2oapp.net/framework/config/schema/object-4.0"
name="Мой объект">
<fields>
<!-- Поля объекта -->
<field id="id"/>
<field id="name"/>
<field id="birthday"/>
<list id="docs">...</list>
...
</fields>
<operations>
<!-- Операции объекта -->
<operation id="create">...</operation>
<operation id="update">...</operation>
<operation id="delete">...</operation>
...
</operations>
<validations>
<!-- Валидации объекта -->
<constraint id="uniqueName">...</constraint>
<condition id="dateInPast">...</condition>
...
</validations>
</object>

Подробнее об объектах

Операции объекта

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

Операция объекта
<operation id="create">
<invocation>
... <!--Провайдер данных-->
</invocation>
<in>
<!--Входные данные-->
<field id="name"/>
<field id="birthday"/>
</in>
<out>
<!--Выходные данные-->
<field id="id"/>
</out>
<fail-out>
<!--Выходные данные в случае ошибки операции-->
<field id="message" mapping="#this.getMessage()"/>
</fail-out>
<validations>...</validations><!--Валидации операций-->
</operation>

Валидации объекта

Валидации — это проверки объекта на корректность.

Проверки могут быть на удовлетворённость данных какому-либо условию. Например, что дата не может быть в прошлом. Они задаются элементом <condition>:

<validations>
<condition id="dateInPast"
on="birthday"
message="Дата рождения не может быть в будущем">
birthday <= today()
</condition>
</validations>

Условия пишутся на языке JavaScript.

Так же проверки могут быть выполнены в базе данных или сервисах. Например, что наименование должно быть уникальным. Такие проверки задаются в элементе <constraint>:

<validations>
<constraint id="uniqueName"
message="Имя {name} уже существует"
result="cnt == 0">
<invocation>
... <!-- Провайдер данных -->
</invocation>
<in>
<!--Входные данные-->
<field id="id"/>
<field id="name"/>
</in>
<out>
<!--Выходные данные-->
<field id="cnt"/>
</out>
</constraint>
</validations>

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

Выборка

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

Выборки создаются с помощью файлов [id].query.xml.

Структура выборки
<?xml version='1.0' encoding='UTF-8'?>
<query xmlns="http://n2oapp.net/framework/config/schema/query-5.0"
object-id="myObject">
<list>...</list> <!--Постраничное получение записей-->
<count>...</count> <!--Получение общего количества записей-->
<unique>...</unique> <!--Получение уникальной записи-->
<filters>...</filters>
<fields>
<!-- Поля выборки -->
<field id="firstName"> ... </field>
<field id="lastName"> ... </field>
</fields>
</query>

За получение списка записей отвечает элемент <list>. За получение общего количества записей — элемент <count>.

А за получение одной уникальной записи — <unique>.

Элементов <list>, <count>, <unique> может быть несколько с разными наборами фильтров (атрибут filters).

Структура выборки
<list filters="firstName, lastName">
...
</list>
danger

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

note

id можно сгенерировать, используя <field id="id" select="false"/>.

Подробнее о выборках

Поля выборки

Поле выборки — это информация о способе получения или сортировки данных одного поля объекта.

Существует три типа полей выборки: простое <field>, составное <reference> и списковое <list>. Составное и списковое поля предназначены для работы со сложными объектами и могут содержать внутри себя все три типа полей выборки.

Поля выборки
<fields>
<field id="name"/>
<reference id="organization">
<field id="code"/>
<list id="employees">
<field id="id"/>
<field id="email"/>
<reference id="address">...</reference>
</list>
</reference>
</fields>

Получение результатов выборки

Для того чтобы получить значения полей выборки, эти поля нужно передать на вход провайдеру данных. По умолчанию все, указанные в query поля, участвуют в выборке. Чтобы поле не участвовало в выборке достаточно указать атрибут select со значением false.

Задание выражения для получения значения поля выборки
<field id="firstName"/>
<!-- поле не участвует в выборке -->
<field id="defaultName" select="false" default-value="test"/>

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

В качестве значения select-expression записывается выражение, которое можно вставить в запрос провайдера с помощью плейсхолдера select.

Задание шаблона select команды sql запроса
<?xml version='1.0' encoding='UTF-8'?>
<query xmlns="http://n2oapp.net/framework/config/schema/query-5.0">
<list>
<sql>SELECT :select FROM mytable</sql>
<list>

<fields>
<field id="firstName" select-expression="t.name as firstName"/>
</fields>
</query>

В результате получится запрос вида SELECT t.name as firstName FROM mytable

note

У каждого провайдера свой синтаксис и набор плейсхолдеров

Атрибут select-expression сложных полей поддерживает иерархическую подстановку выражений вложенных полей. Для этого в теле выражения нужно установить некоторую переменную с плейсхолдером, использующимся в провайдере данных, а затем эту переменную указать в атрибуте select-key

Иерархическая подстановка select-expression для сложных полей GraphQL провайдера данных
<?xml version='1.0' encoding='UTF-8'?>
<query xmlns="http://n2oapp.net/framework/config/schema/query-5.0">
<list>
<graphql>
query MyQuery {
allCar() {
$$select
}
}
</graphql>
</list>

<fields>
<list id="showrooms" select-key="showroomsSelect" select-expression="showrooms { $$showroomsSelect }">
<field id="id" select-expression="id"/>
<field id="name" select-expression="name"/>
<reference id="owner" select-expression="owner { $$ownerSelect }" select-key="ownerSelect">
<field id="name" select-expression="name"/>
<field id="inn" select-expression="inn"/>
</reference>
</list>
</fields>
</query>

В результате подстановки значений атрибутов select-expression внутренних полей вместо переменных showroomsSelect и ownerSelect получится следующий запрос:

query MyQuery {
allCar() {
showrooms {
id
name
owner {
name
inn
}
}
}
}

Сортировка поля выборки

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

В атрибуте sorting-expression указывается выражение для отправки.

Задание выражения сортировки поля
<field id="name" sorting-expression="name :direction" sorting-mapping="['direction']"/>

Переменная :direction содержит в себе направление сортировки: ASC или DESC. Название переменной можно сменить с помощью атрибута sorting-mapping.

Атрибут sorting-expression также может использоваться для подстановки вместо плейсхолдера sorting провайдера данных.

Задание шаблона order by команды sql запроса
<list>
<sql>SELECT t.name FROM mytable t ORDER BY :sorting</sql>
</list>

В результате получится запрос вида SELECT t.name FROM mytable t ORDER BY name :direction

Фильтры выборки

Фильтры задаются в элементе <filters>.

У одного поля выборки может быть несколько фильтров. Различаются они по типу фильтрации.

Каждый из них задаётся соответствующим элементом:

Типы фильтраций

ТипОписаниеТип данных
eqЭквивалентностьЛюбой
likeСтрока содержит подстрокуСтроковые
likeStartСтрока начинается с подстрокиСтроковые
inВходит в списокПростые типы
isNullЯвляется nullЛюбой
containsВходит в множествоСписковые типы
overlapsПересекается с множествомСписковые типы
moreСтрого большеЧисла и даты
lessСтрого меньшеЧисла и даты

Почти на каждый из перечисленных типов есть тип с отрицанием, например, notEq.

Задание фильтров в выборке
<filters>
<!-- Фильтр по "eq" -->
<eq field-id="gender.id" filter-id="gender.id">...</eq>
<!-- Фильтр по "in" -->
<in field-id="gender.id" filter-id="genders*.id">...</in>
</filters>

Для фильтра обязательным является атрибут field-id, в котором указывается идентификатор поля выборки, по которому будет осуществлена фильтрация. Атрибут filter-idиспользуется для указания поля фильтра на странице.

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

Задание выражения фильтрации
<?xml version='1.0' encoding='UTF-8'?>
<query xmlns="http://n2oapp.net/framework/config/schema/query-5.0">
<filters>
<eq field-id="code" filter-id="code">...</eq>
<more field-id="person.birthday" filter-id="birthday">...</more>
</filters>

<fields>
<field id="code"/>
<list id="person">
<field id="id"/>
<field id="name"/>
<field id="birthday"/>
</list>
</fields>
</query>

В теле фильтра можно задать выражение фильтрации.

Задание выражения фильтрации
<filters>
<eq field-id="id" filter-id="id">t.id = :id</eq>
</filters>

Также тело фильтра может быть использовано для подстановки вместо плейсхолдера filters провайдера

Задание шаблона where команды sql запроса
<list>
<sql>SELECT t.name FROM mytable t WHERE :filters</sql>
</list>

Провайдеры данных

Тестовый провайдер

Тестовый провайдер данных предназначен для целей обучения и прототипирования. Он позволяет получать и сохранять данные используя json файлы "заглушки". Тестовый провайдер задается элементом <test>.

Атрибут file указывает на расположение json файла в ресурсах проекта относительно папки /src/resources. Содержимое json файла должно начинаться с массива.

/src/resources/test.json
[
{ "id": 1, "name": "Foo" },
{ "id": 2, "name": "Bar" },
...
]

Получение данных

Для получения всех данных необходимо указать операцию findAll.

Получение всех записей test провайдера
<query>
<list>
<test file="test.json" operation="findAll"/>
</list>
...
</query>
note

Атрибут result-mapping в элементе <list> указывать не нужно, потому что в случае с тестовым провайдером путь к списку всегда в корне json файла.

Для получения одной записи необходимо указать операцию findOne.

Получение одной записи test провайдера
<query>
<unique>
<test file="test.json" operation="findOne"/>
</unique>
...
</query>

Операция findOne отбирает первую запись из отфильтрованного списка.

{ "id": 1, "name": "Foo" }
note

В случае, если данные для полей выборки находятся не в корне json объекта, например, вложены в объект "data"

[
{
"data": {
"id": 1,
"name": "test1"
}
},
...
]

можно задать атрибут result-mapping, чтобы маппинг полей был более простым.

<unique result-mapping="['data']">
<test file="test.json" operation="findOne"/>
</unique>

Пагинация данных

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

Получение количества записей в тестовом провайдере
<count>
<test file="test.json" operation="count"/>
</count>

Маппинг полей

Для маппинга полей выборки достаточно указать название свойства в json объекте, который будет получен после обработки result-mapping и result-normalize. Например, для объекта

{ "id": 1, "name": "Foo" }

маппинг полей будет таким

Маппинг полей тестового провайдера
<query>
...
<fields>
<field id="id" mapping="['id']"/> <!-- 1 -->
<field id="name" mapping="['name']"/> <!-- Foo -->
</fields>
</query>
note

Если идентификатор поля совпадает со свойством в json объекте, то маппинг можно не задавать

<query>
...
<fields>
<field id="id"/> <!-- 1 -->
<field id="name"/> <!-- Foo -->
</fields>
</query>

Фильтрация данных

Для задания фильтров тестового провайдера достаточно указать тип фильтра, идентификатор фильтра filter-id и ссылку на поле, к которому относится фильтр field-id. Фильтрация json файла произойдет автоматически.

Фильтрация данных тестового провайдера
<query>
...
<filters>
<eq field-id="id" filter-id="idEq"/>
<like field-id="name" filter-id="nameLike"/>
</filters>

<fields>
<field id="id"/>
<field id="name"/>
</fields>
</query>

Сортировка данных

Для сортировки списка тестового провайдера достаточно указать sorting="true" в простом поле поддерживающем сортировку. Сортировка произойдет автоматически.

Сортировка данных тестового провайдера
<query>
...
<fields>
<field id="id" sorting="true"/>
<field id="name" sorting="true"/>
</fields>
</query>

Операции над данными

Для добавления данных в json файл необходимо указать тип операции create.

Операции над данными тестового провайдера
<operation id="create">
<invocation>
<test file="test.json"
operation="create"
primary-key-type="integer"
primary-key="id"/>
</invocation>
<in>
<field id="name" mapping="['name']"/>
</in>
<out>
<field id="id" mapping="['id']"/>
</out>
</operation>

Если primary-key-type равен integer для поля id будет сгенерировано число, следующее за максимальным из существующих в json файле. Если primary-key-type равен stringдля поля id будет сгенерирована строка в формате UUID. По умолчанию integer.

Название первичного ключа можно изменить через атрибут primary-key. По умолчанию id.

Операции тестового провайдера

ОперацияОписание
createСоздание записи
updateИзменение записи
deleteУдаление записи
updateManyИзменение нескольких записей
updateFieldИзменение одного поля
deleteManyУдаление нескольких записей
echoВозврат входных данных
findAllПоиск всех записей
findOneПоиск одной записи
countПодсчет общего количества записей

Обработка исключений

У тестового провайдера нет специфических исключений. Любые ошибки тестового провайдера выбрасываются как внутренняя ошибка приложения.

SQL

SQL провайдер позволяет выполнять SQL запросы к базе данных. Запросы задаются в элементе <sql>

<sql>SELECT * FROM mytable</sql>

или в файле указанном в атрибуте file относительно ресурсов проекта

<sql file="/sql/mytable.sql"/>
/src/resources/sql/mytable.sql
SELECT * FROM mytable

Подключение

Задайте в pom.xml следующую зависимость Spring:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

А также зависимость для драйвера базы данных.
Например для PostgreSQL она будет выглядеть следующим образом:

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

Настройки соединения

Для соединения с БД необходимо добавить настройки приложения в файл application.properties

Настройки соединения с БД
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=postgres
spring.datasource.password=postgres

Получение данных

Для получения списка записей достаточно написать SELECT запрос.

<list>
<sql>SELECT * FROM mytable t</sql>
</list>

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

<unique filters="idEq">
<sql>SELECT * FROM mytable t WHERE t.id = :idEq</sql>
</unique>

Можно добавить специальный плейсхолдер :select, чтобы шаблонизировать запрос.

<sql>SELECT :select FROM mytable t</sql>

Плейсхолдер :select будет заменен на значения из атрибута select-expression в полях выборки. Разделителем между разными элементами select-expression будет запятая , . Например, для полей

<field id="id" select-expression="t.id as id"/>
<field id="name" select-expression="t.name as name"/>

итоговый запрос будет

SELECT t.id as id, t.name as name FROM mytable t

Пагинация данных

Для пагинации записей SQL запроса следует использовать плейсхолдеры :limit и :offset.

Пагинация записей SQL запроса
<list>
<sql>SELECT * FROM mytable LIMIT :limit OFFSET :offset</sql>
</list>

Чтобы получить общее количество записей можно использовать другой запрос к БД с функцией агрегации.

Получение количества записей SQL запроса
<count count-mapping="[0]['cnt']">
<sql>SELECT count(*) as cnt FROM mytable</sql>
</count>

В атрибуте count-mapping указывается выражение для получения числа записей.

Маппинг полей

В результате выполнения SQL запроса вернется объект Map<String, Object>, где String алиас столбца запроса, а Object его значение. Если алиас столбца не совпадает с идентификатором поля выборки, необходимо сделать маппинг.

Маппинг полей SQL запроса
<field id="firstName"
mapping="['first_name']"
select-expression="t.name as first_name"/>

Фильтрация данных

Для задания фильтров SQL запроса нужно указать тип фильтра, идентификатор фильтра filter-id и ссылку на поле, к которому относится фильтр field-id.

Фильтрация SQL запроса
<fields>
<field id="id"/>
</fields>
<filters>
<eq field-id="id" filter-id="idEq"/> <!-- Плейсхолдер :idEq -->
</filters>

В этом случае будет доступен PreparedStatement плейсхолдер :idEq равный значению filter-id. Если плейсхолдер нужно переименовать, можно использовать маппинг.

Маппинг плейсхолдера фильтрации SQL запроса
<fields>
<field id="gender.id"/>
</fields>
<filters>
<eq field-id="gender.id"
filter-id="gender.id"
mapping="['gender_id']"/> <!-- Плейсхолдер :gender_id -->
</filters>

Можно добавить специальный плейсхолдер :filters, чтобы шаблонизировать фильтрацию выборки.

<list filters-separator=" and ">
<sql>SELECT * FROM mytable t WHERE :filters</sql>
</list>

Разделителем между разными фильтрами обычно должен быть and, поэтому необходимо задать его атрибутом filters-separator.

Плейсхолдер :filters будет заменен на значения из атрибутов filter-expression в фильтрах выборки, если значение фильтра не будет null.

<filters>
<like field-id="name"
filter-id="nameLike"
filter-expression="t.name like '%'||:nameLike</like>"/>
</filters>

при наличии значения в nameLike итоговый запрос будет

SELECT * FROM mytable t WHERE t.name like '%'||:nameLike
note

Если ни одно значение фильтра не задано, плейсхолдер :filters будет заменен на 1=1.

Сортировка данных

Сортировка записей в SQL задается через блок ORDER BY

<sql>SELECT * FROM mytable ORDER BY name :nameDir</sql>

В поле, поддерживающее сортировку, необходимо добавить атрибут sorting и указать маппинг плейсхолдера для задания направления сортировки с помощью sorting-mapping.

Сортировка данных SQL запроса
<field id="name"
sorting="true"
sorting-mapping="['nameDir']"/>
<!-- Если будет сортировка по полю name
в плейсхолдер :nameDir попадет значение asc или desc -->

Можно добавить специальный плейсхолдер :sorting, чтобы шаблонизировать выражение сортировки.

<sql>SELECT * FROM mytable ORDER BY :sorting</sql>

Плейсхолдер :sorting будет заменен на значения из атрибутов sorting-expression в полях выборки, если по этому полю будет задана сортировка.

<field id="name"
sorting="true"
sorting-mapping="['nameDir']"
sorting-expression="name :nameDir"/>

при наличии сортировки asc по полю name итоговый запрос будет

SELECT * FROM mytable ORDER BY name asc

для сортировки по desc аналогично.

note

Если ни одно направление сортировки не задано, плейсхолдер :sorting будет заменен на 1.

Операции над данными

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

Выполнение операции SQL провайдером
<operation id="create">
<invocation>
<sql>INSERT INTO mytable (first_name, last_name) VALUES (:first_name, :last_name)</sql>
</invocation>
<in>
<field id="firstName" mapping="['first_name']"/>
<field id="lastName" mapping="['last_name']"/>
</in>
<out>
<field id="id" mapping="[0]"/>
</out>
</operation>

Результатом INSERT запроса будет массив значений, добавленных в таблицу. Чтобы получить первичный ключ, необходимо в <out> поле задать маппинг на первое добавленное значение.

Обработка исключений

При возникновении ошибок во время выполнения SQL запроса выбрасывается исключение N2oQueryExecutionException. Из него можно получить исходный запрос query и сообщение от БД message.

Получение данных об ошибке SQL
<operation id="create" fail-message="Не удалось создать запись по причине {error}">
...
<out-fail>
<field id="sql" mapping="query"/> <!-- Исходный запрос -->
<field id="error" mapping="message"/> <!-- Сообщение об ошибке -->
</out-fail>
</operation>

REST

REST провайдер выполняет http запросы к REST сервисам. В теле запроса и ответа используется формат Json.

Запросы задаются в элементе <rest>

<rest>http://localhost:8081/api/myservice</rest>

Настройки соединения

Начальный адрес, например, http://localhost:8081/api, можно опускать в элементах <rest>, если он одинаковый для всех сервисов. Для этого нужно задать настройку n2o.engine.rest.url

application.properties
n2o.engine.rest.url=http://localhost:8081/api

в результате REST запрос сократится до простого указания конечной точки

<rest>/myservice</rest>

Если для соединения используется прокси сервер, можно задать его настройки в атрибутах элемента <rest>

<rest proxy-host="192.168.1.0"
proxy-port="3333">...</rest>

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

MyAppConfiguration.java
@Bean
RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
... //настройки restTemplate
return restTemplate;
}

Получение данных

Для получения списка записей обычно нужно выполнить GET запрос к сервису получения данных.

Получение списка записей REST запроса
<list result-mapping="['content']">
<rest>/mystore/goods</rest>
</list>

В атрибуте result-mapping нужно указать путь к списку объектов в ответе сервиса

GET /mystore/goods
{
"content": [
{ "id": 1, "name": "Товар 1", ... },
{ "id": 1, "name": "Товар 2", ... },
...
]
}

Для получения одной записи по идентификатору в REST сервисах обычно используется параметр пути. Параметр пути можно задать через плейсхолдер. Плейсхолдеры задаются в фигурных скобках {something}.

Получение одной записи REST запроса
<unique filters="idEq">
<rest>/mystore/goods/{idEq}</rest>
</unique>

Пагинация данных

Для пагинации записей REST запроса следует использовать плейсхолдеры {limit} и {offset}.

Пагинация записей REST запроса через offset
<list>
<rest>/mystore/goods?limit={limit}&amp;offset={offset}</rest>
</list>

В качестве альтернативы {offset} можно использовать плейсхолдер {page}.

Пагинация записей REST запроса через page
<list>
<rest>/mystore/goods?size={limit}&amp;page={page}</rest>
</list>
note

Номер страницы {page} по умолчанию начинается с нуля 0, но можно начать нумерацию страниц с 1 задав это в настройке

application.properties
#Паджинация начинается с нуля?
n2o.engine.pageStartsWith0=false

Чтобы получить общее количество записей можно использовать атрибут count-mapping, если REST сервис возвращает общее количество записей вместе со списком записей одной страницы.

Получение количества записей REST запроса
<list result-mapping="['content']" count-mapping="['totalElements']">
<rest>/mystore/goods?size={limit}&amp;page={page}</rest>
</list>

В атрибуте count-mapping указывается путь к свойству, содержащему общее количество записей списка.

GET /mystore/goods?size=10&page=0
{
"content": [
{ "id": 1, "name": "Товар 1", ... },
{ "id": 2, "name": "Товар 2", ... },
...
{ "id": 10, "name": "Товар 10", ... }
],
"totalElements": 100
}

Маппинг полей

В результате выполнения REST запроса вернется объект DataSet. К DataSet можно обращаться как к Map<String, Object>, используя квадратные скобки ['field']. Для получения вложенных свойств используется символ "точка" ['field1.field2'].

Если свойство json не совпадает с идентификатором поля выборки, необходимо сделать маппинг.

Маппинг полей REST запроса
<field id="firstName"
mapping="['first_name']"/>

Фильтрация данных

Для задания фильтров REST запроса нужно указать тип фильтра, идентификатор фильтра filter-id и ссылку на поле выборки field-id.

Фильтрация REST запроса
<fields>
<field id="id"/>
</fields>
<filters>
<eq field-id="id" filter-id="idEq"/> <!-- Плейсхолдер {idEq} -->
</filters>

В этом случае будет доступен плейсхолдер {idEq} равный значению filter-id.

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

Маппинг плейсхолдера фильтрации REST запроса
<filters>
<eq field-id="gender.id"
filter-id="gender.id"
mapping="['gender_id']"/> <!-- Плейсхолдер {gender_id} -->
</filters>

Можно добавить специальный плейсхолдер {filters} в REST запрос, чтобы шаблонизировать фильтрацию выборки.

<list filters-separator="&amp;">
<rest>/mystore/goods?{filters}</rest>
</list>

Разделителем между разными фильтрами в параметрах запроса будет &, поэтому необходимо задать его атрибутом filters-separator.

Плейсхолдер {filters} будет заменен на значения из атрибутов filter-expression в фильтрах выборки, если значение фильтра не будет null.

<filters>
<like field-id="name"
filter-id="nameLike"
filter-expression="name={nameLike}"/>
</filters>

при наличии значения "Ноутбук" в nameLike итоговый запрос будет

/mystore/goods?name=Ноутбук
note

Обычно в REST сервисе заранее прошито как то или иное поле фильтруется на сервере. В таком случае использование разных видов фильтров (<eq>, <like>, <in> и др.) не оказывает влияение на запрос и является чисто семантическим.

note

Если ни одно значение фильтра не задано, плейсхолдер {filters} будет заменен на пустую строку.

Сортировка данных

Сортировка записей в REST сервисах обычно задается в параметрах запроса.

<rest>/mystore/goods?sort=name,{nameDir}</rest>

В поле, поддерживающее сортировку, необходимо добавить атрибут sorting и указать маппинг плейсхолдера для задания направления сортировки с помощью sorting-mapping.

Сортировка данных REST запроса
<field id="name"
sorting="true"
sorting-mapping="['nameDir']"/>
<!-- Если будет сортировка по полю name
в плейсхолдер {nameDir} попадет значение asc или desc -->

Можно добавить специальный плейсхолдер {sorting}, чтобы шаблонизировать выражение сортировки.

<rest>/mystore/goods?{sorting}</rest>

Плейсхолдер {sorting} будет заменен на значения из атрибутов sorting-expression в полях выборки, если по этому полю будет задана сортировка.

<field id="name"
sorting="true"
sorting-mapping="['nameDir']"
sorting-expression="sort=name,{nameDir}"/>

при наличии сортировки asc по полю name итоговый запрос будет

GET /mystore/goods?sort=name,asc

для сортировки по desc аналогично.

note

Если ни одно направление сортировки не задано, плейсхолдер {sorting} будет заменен на пустую строку.

Операции над данными

Для выполнения операций над данными нужно записать соответствующий REST запрос и http метод.

Выполнение операции REST
<operation id="create">
<invocation>
<rest method="POST">/mystore/goods</rest>
</invocation>
<in>
<field id="name" mapping="['name']"/>
<field id="price" mapping="['price']"/>
</in>
<out>
<field id="id" mapping="['id']"/>
</out>
</operation>

Входные параметры запроса будут собраны в DataSet объект, который будет сереализован в json и передан в теле запроса, если http метод POST или PUT. В остальных случаях, входные параметры можно использовать в виде плейсхолдеров в самом запросе.

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

Обработка исключений

При возникновении ошибок во время выполнения REST запроса выбрасывается исключение N2oQueryExecutionException. Из него можно получить исходный запрос query и сообщение от REST сервиса message.

Получение данных об ошибке REST
<operation id="create" fail-message="Не удалось создать запись по причине {error}">
...
<out-fail>
<field id="sql" mapping="query"/> <!-- Исходный запрос -->
<field id="error" mapping="message"/> <!-- Сообщение об ошибке -->
</out-fail>
</operation>

Пример

GraphQL

GraphQl провайдер позволяет выполнять GraphQl запросы к GraphQl сервисам.

Запросы задаются в элементе <graphql>

<graphql endpoint="http://localhost:8081/graphql">
query myQuery() {
goods() {
id
name
age
}
}
</graphql>

Настройки соединения

Адрес GraphQL сервиса, например, http://localhost:8081/graphql, можно опускать в элементах <graphql>, если он одинаковый для всех сервисов. Для этого нужно задать настройку n2o.engine.graphql.endpoint

application.properties
n2o.engine.graphql.endpoint=http://localhost:8081/graphql

в результате достаточно будет задавать только сам GraphQL запрос

<graphql>
query myQuery() {
goods() {
id
name
}
}
</graphql>

Для обеспечения безопасного доступа к backend сервису необходимо использовать атрибут access-token или глобальную настройку n2o.engine.graphql.access-token.

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

MyAppConfiguration.java
@Bean
RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
... //настройки restTemplate
return restTemplate;
}

Получение данных

Для получения списка записей нужно выполнить запрос выборки данных к GraphQL сервису.

Получение списка записей GraphQL запроса
<list result-mapping="['data.goods']">
<graphql>
query myQuery() {
goods() {
id
name
}
}
</graphql>
</list>

В атрибуте result-mapping нужно указать путь к списку объектов в ответе GraphQL сервиса

myQuery { goods() }
{
"data": {
"goods": [
{"id": 1, "name": "Товар 1"},
{"id": 1, "name": "Товар 2"},
...
]
}
}

Для получения одной записи по идентификатору в GraphQL сервисах обычно используется атрибуты метода. Значение атрибута можно задать через плейсхолдер. Плейсхолдеры задаются со знаком "двойной доллар" $$something.

Получение одной записи GraphQL запроса
<unique filters="idEq" mapping="['data.goodById']">
<graphql>
query myQuery() {
goodById(id: $$idEq) {
id
name
}
}
</graphql>
</unique>

Пагинация данных

Для пагинации записей GraphQL запроса следует использовать плейсхолдеры $$size и $$offset.

Пагинация записей GraphQL запроса через offset
<list>
<graphql>
query myQuery() {
goods(first: $$size, offset: $$offset) {
id
name
}
}
</graphql>
</list>

В качестве альтернативы $$offset можно использовать плейсхолдер $$page.

Пагинация записей GraphQL запроса через page
<list>
<graphql>
query myQuery() {
goods(size: $$size, page: $$page) {
id
name
}
}
</graphql>
</list>
note

Номер страницы $$page по умолчанию начинается с нуля 0, но можно начать нумерацию страниц с 1 задав это в настройке

application.properties
#Паджинация начинается с нуля?
n2o.engine.pageStartsWith0=false

Чтобы получить общее количество записей можно использовать атрибут count-mapping, если GraphQL сервис возвращает общее количество записей вместе со списком записей одной страницы.

Получение количества записей GraphQL запроса
<list result-mapping="['data.goods']" count-mapping="['data.aggregateGoods.count']">
<graphql>
query myQuery() {
goods(first: $$size, offset: $$offset) {
id
name
}
aggregateGoods {
count
}
}
</graphql>
</list>

В атрибуте count-mapping указывается путь к свойству, содержащему общее количество записей списка.

myQuery { goods() aggregateGoods() }
{
"data": {
"goods": [
{"id": 1, "name": "Товар 1"},
{"id": 1, "name": "Товар 2"},
...
],
"aggregateGoods": {
"count": 100
}
}
}

Маппинг полей

В результате выполнения GraphQL запроса вернется объект DataSet. К DataSet можно обращаться как к Map<String, Object>, используя квадратные скобки ['field']. Для получения вложенных свойств используется символ "точка" ['field1.field2'].

Если свойство json не совпадает с идентификатором поля выборки, необходимо сделать маппинг.

Маппинг полей GraphQL запроса
<field id="firstName"
mapping="['first_name']"/>

Фильтрация данных

Для задания фильтров GraphQL запроса нужно указать тип фильтра, идентификатор фильтра filter-id и ссылку на поле выборки field-id.

Фильтрация GraphQL запроса
<fields>
<field id="id"/>
</fields>
<filters>
<eq field-id="id" filter-id="idEq"/> <!-- Плейсхолдер $$idEq -->
</filters>

В этом случае будет доступен плейсхолдер $$idEq равный значению filter-id.

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

Маппинг плейсхолдера фильтрации GraphQL запроса
<filters>
<eq field-id="gender.id"
filter-id="gender.id"
mapping="['genderId']"/> <!-- Плейсхолдер $$genderId -->
</filters>

Можно добавить специальный плейсхолдер $$filters в GraphQL запрос, чтобы шаблонизировать фильтрацию выборки.

<list>
<graphql filter-separator=", ">
query myQuery() {
goods(filter: { $$filters }) {
id
name
}
}
</graphql>
</list>

Разделителем между разными фильтрами в параметрах запроса будет , , поэтому необходимо задать его атрибутом filters-separator.

Плейсхолдер $$filters будет заменен на значения из атрибутов filter-expression в фильтрах выборки, если значение фильтра не будет null.

<filters>
<like field-id="name"
filter-id="nameLike"
filter-expression="name: $$nameLike"/>
</filters>

при наличии значения "Ноутбук" в nameLike итоговый запрос будет

query myQuery() {
goods(filter: { name: "Ноутбук" }) {
id
name
}
}
note

Обычно в GraphQL сервисе заранее прошито как то или иное поле фильтруется на сервере. В таком случае использование разных видов фильтров (<eq>, <like>, <in> и др.) не оказывает влияение на запрос и является чисто семантическим.

note

Если ни одно значение фильтра не задано, плейсхолдер $$filters будет заменен на пустую строку.

Сортировка данных

Сортировка записей в GraphQL сервисах обычно задается в атрибутах методов.

<graphql>
query myQuery() {
goods(order: { $$nameDir: name } ) {
id
name
}
}
</graphql>

В поле, поддерживающее сортировку, необходимо добавить атрибут sorting и указать маппинг плейсхолдера для задания направления сортировки с помощью sorting-mapping.

Сортировка данных GraphQL запроса
<field id="name"
sorting="true"
sorting-mapping="['nameDir']"/>
<!-- Если будет сортировка по полю name
в плейсхолдер $$nameDir попадет значение asc или desc -->
note

Если GraphQL принимает вместо "asc" или "desc" что-то другое, можно изменить в настройках:

#Значение asc направления сортировки
n2o.engine.query.asc-expression=asc
#Значение desc направления сортировки
n2o.engine.query.desc-expression=desc

Можно добавить специальный плейсхолдер $$sorting, чтобы шаблонизировать выражение сортировки.

<graphql>
query myQuery() {
goods(order: { $$sorting } ) {
id
name
}
}
</graphql>

Плейсхолдер $$sorting будет заменен на значения из атрибутов sorting-expression в полях выборки, если по этому полю будет задана сортировка.

<field id="name"
sorting="true"
sorting-mapping="['nameDir']"
sorting-expression="$$nameDir: name"/>

при наличии сортировки asc по полю name итоговый запрос будет

query myQuery() {
goods(order: { asc: name } ) {
id
name
}
}

для сортировки по desc аналогично.

note

Если ни одно направление сортировки не задано, плейсхолдер $$sorting будет заменен на пустую строку.

Операции над данными

Для выполнения операций над данными нужно задать GraphQL запрос мутации данных.

Выполнение мутации GraphQL
<operation id="create">
<invocation>
<graphql>
mutation MyMutation {
addGood(input: { name: $$name }) {
good {
id
}
}
}
</graphql>
</invocation>
<in>
<field id="name" mapping="['name']"/>
</in>
<out>
<field id="id" mapping="['data.addGood.good[0].id']"/>
</out>
</operation>

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

Экранирование строковых плейсхолдеров

Если значением плейсхолдера $$ является строка, то значение оборачивается в кавычки, согласно синтаксису GraphQl.

$$name -> "Ann"

Для экранирования кавычек можно воспользоваться плейсхолдером "тройной доллар" $$$.

$$$name -> \"Ann\"

Обработка ошибок GraphQl провайдера

Для специфической обработки ошибок результатов GraphQl сервера требуется включить её в реализации интерфейсов OperationExceptionHandler и QueryExceptionHandler.

Свой обработчик ошибок операций GraphQl сервера
package com.example;

public class MyGraphQlOperationExceptionHandler implements OperationExceptionHandler {
@Override
public N2oException handle(CompiledObject.Operation o, DataSet data, Exception e) {
...
// Обработка ошибки GraphQl сервера
if (e instanceof N2oGraphQlException) {
// Получение ответа, который вернул GraphQl сервер
DataSet result = ((N2oGraphQlException) e).getResponse();
DataList errors = (DataList) result.get("errors");
...
// формирование сообщений валидации по ошибкам
List<ValidationMessage> validationMessages = new ArrayList<>();
for (Object obj : errors) {
DataSet error = (DataSet) obj;
String message = error.getString("message");
String fieldId = error.getString("field");
...
validationMessages.add(new ValidationMessage(message, fieldId, validationId));
}
return new N2oUserException("Ошибка валидации", null, validationMessages);
}
...
}
}

Пример

Java

С помощью java провайдеров можно вызвать метод java класса.

Экземпляр класса можно получить с помощью IoC контейнера EJB или Spring. Либо можно вызвать статический метод класса.

Получение списка записей Java провайдером
<query>
<list>
<java
class="com.example.MyService"
method="getList">
<arguments>
<argument
type="criteria"
class="com.example.MyCriteria"/>
</arguments>
<spring/>
</java>
</list>

<fields>
<field id="name" sorting="true"/>
</fields>
</query>
Выполнение операции Java провайдером
<operation id="create">
<invocation>
<java class="com.example.MyService"
method="create">
<arguments>
<argument
type="entity"
class="com.example.MyEntity"/>
</arguments>
<spring/>
</java>
</invocation>
<in>
<field id="firstName" mapping="[0].firstName"/>
<field id="lastName" mapping="[0].lastName"/>
</in>
</operation>

MongoDB

MongoDB провайдер выполняет запросы к MongoDB базе данных.

Получение списка записей Mongo DB провайдером
<query>
<list>
<mongodb collection-name="person" operation="find"/>
</list>
<count>
<mongodb collection-name="person" operation="countDocuments"/>
</count>

<filters>
<eq field-id="id" filter-id="id"/>
</filters>

<fields>
<field id="id" sorting="true" domain="string">
<field id="name" select-expression="name"/>
</fields>
</query>

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

Пример
<query>
<list>
<mongodb collection-name="person" operation="find"/>
</list>

<filters>
<eq field-id="id" filter-id="id">{ _id: new ObjectId('#id') }</eq>
<like field-id="name" filter-id="nameLike" mapping="['nameLikeMap']">{ name: { $regex: '.*#nameLikeMap.*'}}</like>
<likeStart field-id="name" filter-id="nameStart">{ name: {$regex: '#nameStart.*'}}</likeStart>
<more field-id="birthday" filter-id="birthdayMore">{birthday: {$gte: new ISODate(#birthdayMore)}}</more>
<less field-id="birthday" filter-id="birthdayLess">{birthday: {$lte: new ISODate(#birthdayLess)}}</less>
</filters>

<fields>
<field id="id" mapping="['_id'].toString()" select-expression="_id"
sorting="true" domain="string"/>
<field id="name" select-expression="name" domain="string"
sorting-mapping="['sortName']" sorting-expression="name :sortName"/>
<field id="birthday" domain="localdate"/>
</fields>
</query>

Автоматическая генерация для mongodb провайдера

В mongo db идентификатор записи всегда называется _id и имеет тип ObjectId, в N2O идентификатор записи должен называться id и иметь тип string или integer, поэтому:

  • select-expression для поля id преобразуется в _id с mapping="['_id'].toString()"
  • для всех остальных полей select-expression преобразуется в select-expression="id поля"
  • фильтр eq для поля id <eq field-id="id"/> преобразуется в <eq field-id="id">{ _id: new ObjectId('#id') }</eq> фильтры других типов для поля id необходимо прописывать полностью. Автоматическая генерация сработает только для типа eq.
  • для других полей автоматическая генерация тела фильтра работает для всех типов. Но необходимо учитывать, что она простая (для полей с типом дата необходимо писать самостоятельно, с учетом написания фильтров в mongodb).
  • для поля id сортировка sorting-expression преобразуется в sorting-expression="_id :idDirection"
  • для всех других полей, например name, sorting-expression преобразуется в sorting-expression="name :nameDirection"
Пример
<filters>
<eq field-id="id" filter-id="id"/>
</filters>

<!-- Поле id -->
<fields>
<field id="id" sorting="true" domain="string">
</fields>

<!-- трансформируется в -->
<filters>
<eq filter-id="id">{ _id: new ObjectId('#id') }</eq>
</filters>

<field id="id" mapping="['_id'].toString()" select-expression="_id" domain="string"
sorting-expression="_id :idDirection"/>

<filters>
<like field-id="name" filter-id="nameLike"/>
<likeStart field-id="name" filter-id="nameStart"/>
</filters>

<!-- Поле name -->
<field id="name" domain="string" sorting="true"/>

<!-- трансформируется в -->
<filters>
<like field-id="name" filter-id="nameLike">{ name: { $regex: '.*#nameLike.*'}}</like>
<likeStart field-id="name" filter-id="nameStart">{ name: {$regex: '#nameStart.*'}}</likeStart>
</filters>

<field id="name" select-expression="name" domain="string"
sorting-expression="name :nameDirection"/>

<!-- Для даты тело фильтров необходимо прописывать самостоятельно -->
<filters>
<more field-id="birthday" filter-id="birthdayMore">{birthday: {$gte: new ISODate(#birthdayMore)}}</more>
<less field-id="birthday" filter-id="birthdayLess">{birthday: {$lte: new ISODate(#birthdayLess)}}</less>
</filters>

<field id="birthday" domain="localdate"/>
Выполнение операции MongoDB провайдером
<operation id="create">
<invocation>
<mongodb collection-name="person" operation="insertOne"/>
</invocation>
<in>
<field id="firstName"/>
<field id="lastName"/>
</in>
</operation>

Доступны операции insertOne, updateOne, deleteOne, deleteMany, countDocuments.

Типы данных

Типы данных в N2O предназначены для правильной передачи значений от клиента к провайдерам данных.

Типы данных

ТипОписание
stringСтроки
integerЦелые числа
dateДата и время
localdateЛокальная Дата
localdatetimeЛокальная дата и время
booleanЛогический тип (true / false)
objectОбъект с вложенными свойствами
numericЧисло с точкой без округлений
longБольшое целое число
shortКороткое целое число

Любой из перечисленных типов может образовывать списковый тип данных, если добавить в конец квадратные скобки:

integer[]

Типы данных в XML элементах задаются ключевым словом domain.

Тип integer в поле выборки
<query>
...
<fields>
<field id="gender.id" domain="integer">
...
</field>
</fields>
</query>
Тип integer в параметрах операции
<operation>
...
<in>
<field id="gender.id" domain="integer"/>
</in>
</operation>

Биндинг полей

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

Поле виджета
<input-text id="firstName"/>
Поле выборки
<field id="firstName"> ... </field>
Параметр операции
<field id="firstName"/>

Подобная связь называется биндингом.

Биндинг составных полей

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

<input-select id="gender">
... <!-- Содержит id и name -->
</input-select>

В json представлении модель данных gender выглядит так:

{
"gender": {
"id" : 1,
"name" : "Мужской"
}
}

Если мы хотим использовать только id, можно записать биндинг через "точку":

<field id="gender.id"/> <!-- 1 -->

А также можно использовать составное поле:

<reference id="gender">
<field id="id"/><!-- 1 -->
</reference>

Биндинг интервальных полей

Интервальные поля — это поля, в которых можно задать начало и окончание:

<date-interval id="period">
... <!-- Содержит begin и end -->
</date-interval>

В json представлении модель данных period выглядит так:

{
"period": {
"begin" : "01.01.2018 00:00",
"end" : "31.12.2018 00:00"
}
}

При передаче в два параметра нужно использовать окончание .begin и .end:

<field id="period.begin"/> <!-- 01.01.2018 00:00 -->
<field id="period.end"/> <!-- 31.12.2018 00:00 -->

Тот же кейс с использованием составного поля:

<reference id="period">
<field id="begin"/><!-- 01.01.2018 00:00 -->
<field id="end"/><!-- 31.12.2018 00:00 -->
</reference>

Биндинг полей множественного выбора

Поля множественного выбора позволяют выбрать несколько значений из предложенных вариантов:

<select id="regions" type="multi">
...<!-- Содержит несколько регионов -->
</select>

Модель данных regions в json:

{
"regions": [
{
"id" : 1,
"name" : "Адыгея"
},
{
"id" : 16,
"name" : "Татарстан"
}
]
}

Чтобы в параметре операции собрать только идентификаторы regions необходимо использовать "звёздочку":

<field id="regions*.id"/> <!-- [1,16] -->

Также можно использовать списковое поле:

<list id="regions">
<field id="id"/><!-- [1,16] -->
</list>

Порядок вызова операций полей

Для полей доступны различные варианты преобразования:

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

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

Ко входящим полям относятся:

  • фильтры выборки
  • <in> поля операции объекта

В таком случае цепочка начинается с установки значения по умолчанию и завершается маппингом.

default -> normalize -> switch -> mapping

note

Стоит отметить, что для фильтров нет возможности использовать switch

К исходящим полям относятся:

  • поля выборки
  • <out> поля операции объекта

В этом случае, наоборот, маппинг является начальной операцией

mapping -> default -> normalize -> switch

Значение по умолчанию

Маппинг данных в провайдерах

Входные и выходные параметры провайдера могут несоответствовать полям ввода. Для их приведения в соответствие используется атрибут mapping. Выражение в mapping записывается на языке SpEL.

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

note

Java провайдеры в качестве альтернативы могут использовать тип входных параметров "массив значений". Поэтому в маппинге java можно также обращаться по индексу аргумента, например, [0], [1].name.

Маппинг фильтров

Маппинг фильтров в sql, rest, graphql и mongodb

Маппинг фильтров в sql провайдере
<query>
<list>
<sql>SELECT t.first_name, t.gender_id FROM mytable t WHERE :filters</sql>
</list>

<filters>
<!-- Маппинг определяет ключ "first_name" в который будет скопировано значение фильтра "firstName" -->
<like field-id="firstName" filter-id="firstName" mapping="['first_name']">
t.first_name like '%'||:first_name||'%'
</like>
<!-- Маппинг определяет ключ "gender_id" в который будет скопирован id фильтра "gender" -->
<eq field-id="gender.id" filter-id="gender.id" mapping="['gender_id']">
t.gender_id = :gender_id
</eq>
<!-- Маппинг определяет ключ "genders" в который будет скопированы список id из фильтра "genders" -->
<in field-id="gender.id" filter-id="genders*.id" mapping="['genders']">
t.gender_id in (:genders)
</in>
</filters>

<fields>
<field id="firstName"/>
<field id="gender.id"/>
</fields>
</query>
Маппинг фильтров в rest провайдере
<query>
<list>
<rest>/api/myentity/items?{filters}</rest>
</list>

<filters>
<!-- Маппинг определяет ключ "gender_id" в который будет скопирован id фильтра "gender" -->
<eq field-id="gender.id" filter-id="gender.id" mapping="['gender_id']">
gender_id={gender_id}
</eq>
<!-- Маппинг определяет ключ "genders" в который будет скопированы список id из фильтра "genders" -->
<in field-id="gender.id" filter-id="genders*.id" mapping="['genders']">
gender_id_in={genders}
</in>
<!-- Маппинг определяет ключ "first_name" в который будет скопировано значение фильтра "firstName" -->
<like field-id="firstName" filter-id="firstName" mapping="['first_name']">
first_name_like={first_name}
</like>
</filters>

<fields>
<field id="firstName"/>
<field id="gender.id"/>
</fields>
</query>
Маппинг фильтров в graphql провайдере
<query>
<list>
<graphql filter-separator=", and:">
query posts(filter: $$filters) {
score
completed
}
</graphql>
</list>

<filters>
<eq field-id="score" filter-id="score">{ score: {gt: $$score} }</eq>
<eq field-id="completed" filter-id="completed">{ completed: $$completed }</eq>
...
</filters>

<fields>
<field id="score" mapping="['score']"/>
<field id="completed" mapping="['completed']"/>
...
</fields>
</query>
Маппинг сортировки в mongodb провайдере
<query>
<list>
<mongodb collection-name="user" operation="find"/>
</list>
<fields>
<!-- Маппинг определяет ключ "sortUserAge" в который будет скопировано значение фильтра поля "userAge" -->
<field id="userAge" domain="integer"
sorting-mapping="['sortUserAge']" sorting-expression="age :sortUserAge"/>
</fields>
</query>

Маппинг входных параметров операции

Маппинг входных параметров операции sql

Маппинг входных параметров в sql провайдере
<operation>
<invocation>
<sql>INSERT INTO mytable (first_name, gender_id) VALUES (:first_name, :gender_id)</sql>
</invocation>
<in>
<field id="name" mapping="['first_name']"/>
<field id="gender.id" mapping="['gender_id']"/>
</in>
</operation>
Маппинг входных параметров в graphql провайдере
<operation>
<invocation>
<graphql>mutation { create(score: $$user_score, completed: $$is_completed) {id score completed}</graphql>
</invocation>
<in>
<field id="score" mapping="['user_score']"/>
<field id="completed" mapping="['is_completed']"/>
</in>
</operation>

Маппинг входных параметров операции rest

Запрос rest
POST /api/myentity
Тело запроса
{
"firstName" : "John",
"genderId" : 1
}
Маппинг входных параметров в rest провайдере
<operation>
<invocation>
<rest method="post">/api/myentity</rest>
</invocation>
<in>
<field id="name" mapping="['firstName']"/>
<field id="gender.id" mapping="['genderId']"/>
</in>
</operation>

Маппинг входных параметров операции java

Для вызова метода java класса необходимо передать аргументы вызова в виде массива Object[]. В java провайдере можно задать класс каждого аргумента. Существует 3 типа аргументов: примитивы, сущности, критерии.

Типы аргументов java провайдера

ТипОписание
primitiveПримитивные java классы: String, Integer, Boolean и т.п. Для них не нужно задавать атрибут class.
entityКласс сущности. Для них не нужно задавать атрибут class, если в объекте задан атрибут entity-class.
criteriaКласс, содержащий фильтры, сортировки и паджинацию.
Маппинг примитивов

Предположим у нас есть метод java класса с примитивным типом аргументов:

Метод java класса с примитивным типом аргументов
package com.example;

class Calculator {
public static Long sum(Long a, Long b) {
return a + b;
}
}

Чтобы смапить значение поля ввода в примитивный аргумент java метода, достаточно указать имя аргумента:

Маппинг примитивов в java провайдере
<operation>
<invocation>
<java class="com.example.Calculator" method="sum">
<arguments>
<argument type="primitive" name="arg1"/>
<argument type="primitive" name="arg2"/>
</arguments>
</java>
</invocation>
<in>
<field id="a" mapping="['arg1']"/>
<field id="b" mapping="['arg2']"/>
</in>
</operation>
note

Аргументы должны быть указаны в том же порядке, что и соответствующие аргументы java метода

Маппинг сущности
Метод java класса с аргументом - сущностью
@Service
class MyService {
public Long create(MyEntity entity) { ... }
}
java
class MyEntity {
private String name;
private String surname;
//getters and setters
}

Тип entity может быть задан только один раз среди всех аргументов.

Маппинг сущности в java провайдере
<operation>
<invocation>
<java class="com.example.MyService" method="create">
<arguments>
<argument type="entity" class="com.example.MyEntity"/>
</arguments>
<spring/>
</java>
</invocation>
<in>
<field id="firstName" mapping="[0].name"/>
<field id="lastName" mapping="[0].surname"/>
</in>
</operation>
Маппинг критериев

Критерии предназначены для передачи параметров фильтрации, сортировки и паджинации в java провайдер. Как правило, фильтры задаются через поля класса, т.к. они уникальны для каждого случая. А сортировка и паджинация задаются через базовый класс наследник. N2O поддерживает несколько базовых классов критериев:

ТипОписание
org.springframework.data.domain.PageableИнтерфейс библиотеки spring-data для задания паджинации
org.springframework.data.domain.SortКласс библиотеки spring-data для задания сортировок
org.springframework.data.domain.ExampleИнтерфейс библиотеки spring-data для задания критериев по полям сущности
net.n2oapp.criteria.CriteriaКласс библиотеки criteria-api для задания сортировок и паджинации
Метод java класса с аргументом - критерием
@Service
class MyService {
public List<MyEntity> getList(MyCriteria criteria) { ... }
}
class MyCriteria extends Criteria {
private Date birtdayBefore;
private Date birtdayAfter;
//getters and setters
}

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

Задание фильтров в java провайдере
<query>
<list>
<java
class="com.example.MyService"
method="getList">
<arguments>
<argument
type="criteria"
class="com.example.MyCriteria"/>
</arguments>
<spring/>
</java>
</list>

<filters>
<more field-id="birthday" filter-id="birthdays.begin" mapping="[0].birthdayAfter"/>
<less field-id="birthday" filter-id="birthdays.end" mapping="[0].birthdayBefore"/>
</filters>

<fields>
<field id="birthday" sorting="true"/>
</fields>
</query>

Маппинг результатов выборки

Выборка возвращает список объектов при вызове через <list>, или один объект, при вызове через <unique>. Задача маппинга — задать соответствие между свойством вернувшегося объекта и полем выборки.

Маппинг результатов выборки sql

Sql запрос
SELECT name as fname, surname as lname FROM mytable
Маппинг результатов выборки sql провайдера
<query>
<list>
<sql>SELECT name as fname, surname as lname FROM mytable</sql>
</list>
<count>
<sql>SELECT count(*) FROM mytable</sql>
</count>

<fields>
<field id="firstName" mapping="['fname']"/>
<field id="lastName" mapping="['lname']"/>
</fields>
</query>

Маппинг результатов выборки graphql

GraphQl запрос
query persons() {name age}
Маппинг результатов выборки sql провайдера
<query>
<list result-mapping="['data.persons']">
<graphql>
query persons() {
name
age
}
</graphql>
</list>

<fields>
<field id="personName" mapping="['name']"/>
<field id="personAge" mapping="['age']"/>
</fields>
</query>

Маппинг результатов выборки rest

Запрос rest сервиса
GET /api/myentity/items
Ответ rest сервиса
{
"data" : [
{
"name" : "John",
"surname" : "Doe"
},
...
],
"cnt" : 123
}
Маппинг результатов выборки из rest провайдера
<query>
<list>
<rest
result-mapping="data"
count-mapping="cnt">/api/myentity/items</rest>
</list>

<fields>
<field id="firstName" mapping="['name']"/>
<field id="lastName" mapping="['surname']"/>
</fields>
</query>

Маппинг результатов выборки java

Метод java класса, возвращающий Spring Data Page
@Repository
interface MyRepository extends JpaRepository<MyEntity, Long> {
Page<MyEntity> findAll();
}
class MyEntity {
private String name;
private String surname;
//getters and setters
}
Маппинг результатов выборки в java провайдере
<query>
<list
result-mapping="content"
count-mapping="totalElements">
<java
class="com.example.MyRepository"
method="findAll">
<spring/>
</java>
</list>

<fields>
<field id="firstName" mapping="['name']"/>
<field id="lastName" mapping="['surname']"/>
</fields>
</query>
Маппинг результатов выборки в mongodb провайдере
<query>
<list>
<mongodb collection-name="user" operation="find"/>
</list>
<fields>
<!-- маппинг определяет из какого поля результатов выборки из бд взять значение для userAge -->
<field id="userAge" mapping="['age']" select-expression="age"
domain="integer"/>
</fields>
</query>

Маппинг вложенных полей

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

Ответ провайдера данных

[
{
"id": 1001,
"departments": [
{
"name": "department1",
"manager": {
"id": 1,
"name": "manager1"
}
},
{
"name": "department2",
"manager": {
"id": 2,
"name": "manager2"
}
}
]
}
]
Маппинг результатов
<fields>
<field id="orgId" mapping="['id']"/>

<list id="orgDepartments" mapping="['departments']">
<field id="depName" mapping="['name']"/>
<reference id="depManager" mapping="['manager']">
<field id="managerId" mapping="['id']"/>
<field id="managerName" mapping="['name']"/>
</reference>
</list>
</fields>

Маппинг результатов операции

Чтобы вернуть данные от провайдера, после выполнения операции, используется элемент <out>:

Маппинг результатов sql

Получение результата выполнения sql провайдера
<operation>
<invocation>
<sql>INSERT INTO mytable (first_name, gender_id) VALUES (:first_name, :gender_id)</sql>
</invocation>
<out>
<field id="id" mapping="[0]"/>
</out>
</operation>

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

Маппинг результатов rest

Запрос rest
POST /api/myentity
Ответ rest
{
"result" : 123
}
Получение результата выполнения rest провайдера
<operation>
<invocation>
<rest method="post">/api/myentity</rest>
</invocation>
<out>
<field id="id" mapping="['result']"/>
</out>
</operation>

Маппинг результатов mongodb

Операция insertOne возвращает всегда id, операции updateOne, deleteOne и deleteMany не возвращают ничего, поэтому маппинг результатов имеет место только для insertOne.

Пример
<operation id="create">
<invocation>
<mongodb collection-name="user" operation="insertOne"/>
</invocation>
<in>
<field id="name" mapping="['name']"/>
<field id="age" mapping="['age']"/>
</in>
<out>
<field id="id" mapping="#this"/>
</out>
</operation>

Маппинг данных в Entity

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

Для этого в объекте есть специальный атрибут entity-class и список полей <fields>:

Определение entity класса в объекте
<object entity-class="com.example.MyEntity">
<fields>
...<!--Маппинг полей Entity-->
</fields>
</object>

Маппинг простых полей сущности

Поля делятся на простые и составные.

Простые поля имеют примитивный тип данных (Integer, String, Date и т.п.) Составные поля либо ссылаются на другие N2O объекты, либо имеют вложенные поля.

Класс сущности с простыми полями
@Entity
class MyEntity {
@Id
@Column
private Long id;
@Column
private Date birtDate;
//getters and setters
}
Маппинг простых полей
<object entity-class="com.example.MyEntity">
<fields>
<!-- Простые поля -->
<field id="id" domain="long" mapping="['id']"/>
<field id="birthday" domain="date" mapping="['birthDate']"/>
</fields>
</object>

Атрибут id задаёт поле виджета, атрибут mapping - поле сущности.

Маппинг полей @ManyToOne и @OneToOne

Класс сущности с @ManyToOne и @OneToOne
@Entity
class MyEntity {
@ManyToOne
private Gender gender;
@OneToOne
private Address addr;
//getters and setters
}
Маппинг полей с @OneToOne
<fields>
<reference id="address"
mapping="['addr']">
<!-- Вложенные поля -->
<field id="home" domain="string"/>
<field id="work" domain="string"/>
</reference>
</fields>
Вариант с определением полей во внешнем файле
<fields>
<reference id="address"
mapping="['addr']"
object-id="address"/> <!-- Ссылка на объект address.object.xml -->
</fields>
address.object.xml
<!-- обратите внимание, что класс сущности может быть указан во внешнем файле -->
<object xmlns="http://n2oapp.net/framework/config/schema/object-4.0"
entity-class="org.example.Address">
<fields>
<field id="home" domain="string"/>
<field id="work" domain="string"/>
</fields>
</object>
note

Описывать поля можно внутри составного поля (reference, list или set) или во внешнем файле. Однако, если вы опишите поля в обоих местах, то более приоритетным будет вариант задания полей внутри составного поля. Все поля будут взяты из него, а для полей присутствующих в обоих файлах (т.е. с совпадающим id) будет произведено слияние в пользу текущего объекта.

Маппинг полей с @ManyToOne
<fields>
<reference id="sex"
mapping="['gender']"
required="true"
object-id="gender"/>
</fields>

Маппинг полей @OneToMany и @ManyToMany

Поля объекта могут быть множественными. Есть несколько типов множественности:

Типы множественности

ТипОписание
listСписок значений
setНабор значений
Класс сущности с множественными полями
@Entity
class MyEntity {
@OneToMany
private Set<Status> statuses;
@ManyToMany
private List<Address> addrs;
//getters and setters
}
Маппинг полей с @OneToMany и @ManyToMany
<fields>
<set id="statuses"
mapping="['statuses']">
<!--Вложенные поля-->
<field id="id" domain="integer"/>
<field id="name" domain="string"/>
</set>
<list id="addresses"
mapping="['addrs']"
object-id="address"/><!--Ссылка на объект-->
</fields>

Использование полей объекта

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

@Service
class MyService {
MyEntity create(MyEntity entity) { ... }
}
Использование полей объекта в операции
<object entity-class="com.example.Employee">
<!-- Маппинг и структура полей сущности -->
<fields>
<field id="name" mapping="['name']" normalize="..."/>
<reference id="org" mapping="['organization']" object-id="org"/>
<list id="departments" entity-class="com.example.Department">
<field id="id"/>
<field id="name"/>
</list>
</fields>

<operations>
<operation>
<invocation>
<java method="create">
<arguments>
<argument type="entity"/>
</arguments>
<spring/>
</java>
</invocation>
<in>
<!-- Перечисление только нужных полей -->
<!-- Не нужно задавать ни структуру, ни атрибуты -->
<field id="name"/>
<reference id="org"/>
<list id="departments"/>
</in>
<out>
<field id="id"/>
</out>
</operation>
</operations>
</object>

Нормализация данных

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

Нормализация поля выборки
<fields>
<field id="title" normalize="#this.toUpperCase()"/>
</fields>
note

Нормализацию также можно применять на данные, полученные выборкой в ответе от провайдера. Для записи нормализующего выражения в тегах <list> и <unique> используется атрибут result-normalize.

Ключевые слова

#this

Позволяет ссылаться на значение текущего поля, полученное после маппинга

Использование ключевого слова #this
<fields>
<field id="name" normalize="#this != null ? #this : 'default'"/>
</fields>

#data

Позволяет ссылаться на значение другого поля (на том же уровне вложенности), полученного после маппинга

Использование ключевого слова #data
<fields>
<field id="beginDate"/>
<field id="endDate"/>
<field id="period" normalize="#data['beginDate'] + ' - ' + #data['endDate']"/>
</fields>

#parent

Позволяет ссылаться на значение другого поля (на уровень выше), полученного после маппинга

Использование ключевого слова #parent
<fields>
<field id="name"/>
<reference id="org">
<field id="fullName" normalize="#parent['name'] + '_' + #this"/>
</reference>
</fields>

Вызовы функций нормализации

В выражениях нормализации могут быть использованы:

  1. Статические java-функции
Использование статической java-функций в выражении нормализации
<fields>
<field id="max" normalize="T(java.lang.Math).max(1000, #this)"/>
</fields>
  1. Нестатические java-функции
Использование нестатической java-функций в выражении нормализации
<fields>
<field id="random" normalize="new java.util.Random().nextInt(100)"/>
</fields>
  1. Пользовательские классы
Использование пользовательских классов в выражении нормализации
<fields>
<field id="currentDate" normalize="T(org.my_project.app.MyDateUtil).getCurrentDate()"/>
</fields>

Встроенные функции нормализации

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

Функции нормализации строковых данных StringNormalizer

Имя методаОписание
jsonToMapПреобразование строки json в объект
mapToJsonПреобразование объекта в строку json
encodeToBase64Преобразование строки из UTF-16 в base64
decodeFromBase64Преобразование строки из base64 в UTF-16
formatByMaskФорматирование значения по маске
formatFullNameФорматирование ФИО без сокращений
formatNameWithInitialsФорматирование ФИО с инициалами

jsonToMap

Преобразование строки json в объект

АргументТипОписание
jsonСтрокаСтрока json

Данные

Входные данные:
"{ \"id\": 1,\"name\": \"test\" }"

Выходные данные:
{
"id": 1,
"name": "test"
}

Пример

<reference id="person" normalize="#jsonToMap(#this)">
<field id="id"/>
<field id="name"/>
</reference>

mapToJson

Преобразование объекта в строку json

АргументТипОписание
mapОбъектОбъект \ Map

Данные

Входные данные:
{
"id": 1,
"name": "test"
}

Выходные данные:
"{ \"id\": 1,\"name\": \"test\" }"

Пример

<field id="organization" normalize="#mapToJson(#this)"/>

encodeToBase64

Преобразование строки из UTF-16 в base64

АргументТипОписание
textСтрокаСтрока в UTF-16

Данные

Входные данные:
"test"

Выходные данные:
"dGVzdA=="

Пример

<field id="encoded" normalize="#encodeToBase64(#this)"/>

decodeFromBase64

Преобразование строки из base64 в UTF-16

АргументТипОписание
base64СтрокаСтрока в base64

Данные

Входные данные:
"dGVzdA=="

Выходные данные:
"test"

Пример

<field id="decoded" normalize="#decodeFromBase64(#this)"/>

formatByMask

Форматирование значения по маске

АргументТипОписание
valueОбъектФорматируемое значение
maskСтрокаМаска

Данные

Входные данные:
"1234567890"

Выходные данные:
"(123) 456-78-90"

Пример

<field id="phone" normalize="#formatByMask(#this, '(###) ###-##-##')"/>

formatFullName

Форматирование ФИО без сокращений

АргументТипОписание
namesСписок строкСписок ФИО (допускается ФИ, ИО и т.д.)

Данные

Входные данные:
"Лев", "Николаевич", "Толстой"

Выходные данные:
"Лев Николаевич Толстой"

Пример

<field id="fio" normalize="#formatFullName(#this)"/>
<field id="fullName" normalize="#formatFullName(#data['lastName'], #data['firstName'], #data['patronymic'])"/>

formatNameWithInitials

Форматирование ФИО с инициалами

АргументТипОписание
namesСписок строкСписок ФИО (допускается ФИ, ИО и т.д.)

Данные

Входные данные:
"Толстой", "Лев", "Николаевич"

Выходные данные:
"Толстой Л.Н."

Пример

<field id="fio" normalize="#formatNameWithInitials(#this)"/>
<field id="shortName" normalize="#formatNameWithInitials(#data['lastName'], #data['firstName'], #data['patronymic'])"/>

Функции нормализации даты и времени DateFormatNormalizer

Имя методаОписание
dateПреобразование даты из формата ISO в формат dd.MM.yyyy
dateWithOutputПреобразование даты из формата ISO в указанный формат
dateWithInputПреобразование даты из указанного формата в формат dd.MM.yyyy
dateWithInputAndOutputПреобразование даты из входного формата в выходной
periodПреобразование двух дат к интервальному виду

date

Преобразование даты из формата ISO в формат dd.MM.yyyy

АргументТипОписание
dateStrСтрокаСтрока в формате ISO

Данные

Входные данные:
"2022-09-12"

Выходные данные:
"12.09.2022"

Пример

<field id="dateDefaultFormats" normalize="#date(#this)"/>

dateWithOutput

Преобразование даты из формата ISO в указанный формат

АргументТипОписание
dateStrСтрокаСтрока в формате ISO
outputFormatСтрокаВыходной формат даты

Данные

Входные данные:
"2022-09-12", "dd.M.yyyy"

Выходные данные:
"12.9.2022"

Пример

<field id="dateOutputFormat" normalize="#dateWithOutput(#this, 'dd.M.yyyy')"/>

dateWithInput

Преобразование даты из указанного формата в формат dd.MM.yyyy

АргументТипОписание
dateStrСтрокаСтрока в входном формате
inputFormatСтрокаВходной формат даты

Данные

Входные данные:
"9.9.2022", "d.M.yyyy"

Выходные данные:
"09.09.2022"

Пример

<field id="dateInputFormat" normalize="#dateWithInput(#this, 'd.M.yyyy')"/>

dateWithInputAndOutput

Преобразование даты из входного формата в выходной

АргументТипОписание
dateStrСтрокаСтрока в входном формате
inputFormatСтрокаВходной формат даты
outputFormatСтрокаВыходной формат даты

Данные

Входные данные:
"3.8.2011 10:15:00", "d.M.yyyy HH:mm:ss", "yyyy-MM-dd''T''HH:mm:ss"

Выходные данные:
"2011-08-03T10:15:00"

Пример

<field id="dateTimeInputAndOutputFormat" normalize="#dateWithInputAndOutput(#this, 'd.M.yyyy HH:mm:ss', 'yyyy-MM-dd''T''HH:mm:ss')"/>

period

Преобразование двух дат к интервальному виду

АргументТипОписание
startDateСтрокаНачальное значение даты
endDateСтрокаКонечное значение даты

Данные

Входные данные:
"12.09.2022", "13.09.2022"

Выходные данные:
"12.09.2022 - 13.09.2022"

Пример

<field id="datePeriod" normalize="#period(#data['startDate'], #data['endDate'])"/>

Добавление собственных функций

Указание полного имени класса для вызова пользовательских функций является не очень удобным. Однако существует возможность вызывать пользовательские функции таким же образом, что и встроенные, например normalize="#myFunction(#this)". Для этого достаточно сделать их открытыми (public) и указать над ними аннотацию @Normalizer

Пользовательская функция нормализации
@Normalizer
public static String shorten(String text, Integer length) {
if (text == null)
return "";
if (text.length < length)
text;
else
text.substring(0, length - 1) + "...";
}
Вызов пользовательской функции нормализации
<fields>
<field id="description" normalize="#shorten(#this, 100)"/>
</fields>

Из-за особенностей хранения данных в контексте SpEL в него нельзя добавить перегруженные методы, однако с помощью атрибута value аннотации @Normalizer можно указать алиас функции, который будет использоваться для ссылки в xml.

Алиас функции нормализации
@Normalizer("shortenTo40")
public static String shorten(String text) {
if (text == null)
return "";
if (text.length < DEFAULT_LENGTH)
text;
else
text.substring(0, DEFAULT_LENGTH - 1) + "...";
}
Алиас функции нормализации
<fields>
<field id="description" normalize="#shortenTo40(#this)"/>
</fields>
note

Аннотацией @Normalizer также можно пометить класс. В таком случае в контекст будут добавлены все открытые статические методы этого класса.

Наконец, переопределите настройку n2o.engine.normalizer.packages для указания пакетов, содержащих классы и методы, помеченные аннотацией @Normalizer.

n2o.engine.normalizer.packages=net.n2oapp, org.mypackage1, org.mypackage2

Нормализация вложенных полей

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

Порядок обработки сложных полей:

  • Маппинг родительского поля
  • Нормализация родительского поля
  • Маппинг дочернего поля
  • Нормализация дочернего поля

Допустим, что провайдером получены следующие данные

Ответ провайдера данных
{
"organization": {
"code": 2,
"title": "org2"
}
}

Также допустим, что имеется некоторая функция нормализации normalizeOrganization(DataSet org), которая преобразует исходный json к виду:

Результат преобразования функцией normalizeOrganization
{
"organization": {
"codeStr": "2",
"upperCaseTitle": "ORG2"
}
}

Тогда пример обработки такого поля выборки может выглядеть следующим образом:

Порядок обработки сложных полей
<fields>
<reference id="mainOrg" mapping="['organization']"
normalize="#normalizeOrganization(#this)">
<!-- после маппинга и нормализации родительского поля аналогичные действия производятся и для дочерних полей -->
<field id="orgCode" mapping="['codeStr']" normalize="'code: ' + #this"/>
<field id="orgTitle" mapping="['upperCaseTitle']" normalize="'title: ' + #this"/>
</reference>
</fields>
Результат, полученный после обработки всеми нормализаторами
{
"organization": {
"codeStr": "code: 2",
"upperCaseTitle": "title: ORG2"
}
}

Переключатель switch

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

Вычисление цвета по статусу с помощью нормализации
<field id="color" mapping="['status']"
normalize="#this == 'New' ? 'success' : #this == 'In Progress' ? 'warning' : 'info'"/>

Но чем больше будет вариантов для соотнесения, тем более сложным и плохо поддерживаемым будет итоговое выражение.

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

Вычисление цвета по статусу с помощью конструкции switch
<field id="color" mapping="['status']">
<switch>
<case value="New">success</case>
<case value="In Progress">warning</case>
<case value="Blocked">danger</case>
<default>info</default>
</switch>
</field>

Обратите внимание, что в элементе <switch> не нужно указывать ни value, ни value-field-id, типичные для этой конструкции. В качестве значения, с которым будут сравниваться варианты <case>, мы рассматриваем значение текущего поля, получившееся после предыдущих этапов обработки (маппинг, нормализация и т.д.).

В данном случае конструкция <switch> является хорошей альтернативой нормализации, но они также могут работать и в связке друг с другом.