Провайдеры данных
В 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>
В выборке для таблицы обязательно должно быть поле id.
Без id нельзя будет выбрать конкретную запись и совершить с ней какие-либо действия.
Если в таблице записи будут иметь одинаковые id, то все они будут одновременно выделены.
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
.
<?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
У каждого провайдера свой синтаксис и набор плейсхолдеров
Атрибут select-expression
сложных полей поддерживает иерархическую подстановку выражений вложенных полей. Для этого в теле выражения
нужно установить некоторую переменную с плейсхолдером, использующимся в провайдере данных,
а затем эту переменную указать в атрибуте select-key
<?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
провайдера данных.
<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
провайдера
<list>
<sql>SELECT t.name FROM mytable t WHERE :filters</sql>
</list>
Провайдеры данных
Тестовый провайдер
Тестовый провайдер данных предназначен для целей обучения и прототипирования.
Он позволяет получать и сохранять данные используя json файлы "заглушки".
Тестовый провайдер задается элементом <test>
.
Атрибут file
указывает на расположение json файла в ресурсах проекта относительно папки /src/resources
.
Содержимое json файла должно начинаться с массива.
[
{ "id": 1, "name": "Foo" },
{ "id": 2, "name": "Bar" },
...
]
Получение данных
Для получения всех данных необходимо указать операцию findAll
.
<query>
<list>
<test file="test.json" operation="findAll"/>
</list>
...
</query>
Атрибут result-mapping
в элементе <list>
указывать не нужно, потому что в случае с тестовым провайдером путь к списку всегда в корне json файла.
Для получения одной записи необходимо указать операцию findOne
.
<query>
<unique>
<test file="test.json" operation="findOne"/>
</unique>
...
</query>
Операция findOne
отбирает первую запись из отфильтрованного списка.
{ "id": 1, "name": "Foo" }
В случае, если данные для полей выборки находятся не в корне 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>
Если идентификатор поля совпадает со свойством в 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"/>
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
.
<list>
<sql>SELECT * FROM mytable LIMIT :limit OFFSET :offset</sql>
</list>
Чтобы получить общее количество записей можно использовать другой запрос к БД с функцией агрегации.
<count count-mapping="[0]['cnt']">
<sql>SELECT count(*) as cnt FROM mytable</sql>
</count>
В атрибуте count-mapping
указывается выражение для получения числа записей.
Маппинг полей
В результате выполнения SQL запроса вернется объект Map<String, Object>
,
где String
алиас столбца запроса, а Object
его значение.
Если алиас столбца не совпадает с идентификатором поля выборки, необходимо сделать маппинг.
<field id="firstName"
mapping="['first_name']"
select-expression="t.name as first_name"/>
Фильтрация данных
Для задания фильтров SQL запроса нужно указать тип фильтра, идентификатор фильтра filter-id
и ссылку на поле, к которому относится фильтр field-id
.
<fields>
<field id="id"/>
</fields>
<filters>
<eq field-id="id" filter-id="idEq"/> <!-- Плейсхолдер :idEq -->
</filters>
В этом случае будет доступен PreparedStatement
плейсхолдер :idEq
равный значению filter-id
.
Если плейсхолдер нужно переименовать, можно использовать маппинг.
<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
Если ни одно значение фильтра не задано, плейсхолдер :filters
будет заменен на 1=1
.
Сортировка данных
Сортировка записей в SQL задается через блок ORDER BY
<sql>SELECT * FROM mytable ORDER BY name :nameDir</sql>
В поле, поддерживающее сортировку, необходимо добавить атрибут sorting
и указать маппинг плейсхолдера для задания направления сортировки с помощью sorting-mapping
.
<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
аналогично.
Если ни одно направление сортировки не задано, плейсхолдер :sorting
будет заменен на 1
.
Операции над данными
Для выполнения операций над данными нужно записать соответствующий 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
.
<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
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 провайдер выполняет запросы.
@Bean
RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
... //настройки restTemplate
return restTemplate;
}
Получение данных
Для получения списка записей обычно нужно выполнить GET
запрос к сервису получения данных.
<list result-mapping="['content']">
<rest>/mystore/goods</rest>
</list>
В атрибуте result-mapping
нужно указать путь к списку объектов в ответе сервиса
{
"content": [
{ "id": 1, "name": "Товар 1", ... },
{ "id": 1, "name": "Товар 2", ... },
...
]
}
Для получения одной записи по идентификатору в REST сервисах обычно используется параметр пути.
Параметр пути можно задать через плейсхолдер.
Плейсхолдеры задаются в фигурных скобках {something}
.
<unique filters="idEq">
<rest>/mystore/goods/{idEq}</rest>
</unique>
Пагинация данных
Для пагинации записей REST запроса следует использовать плейсхолдеры {limit}
и {offset}
.
<list>
<rest>/mystore/goods?limit={limit}&offset={offset}</rest>
</list>
В качестве альтернативы {offset}
можно использовать плейсхолдер {page}
.
<list>
<rest>/mystore/goods?size={limit}&page={page}</rest>
</list>
Номер страницы {page}
по умолчанию начинается с нуля 0
, но можно начать нумерацию страниц с 1
задав это в настройке
#Паджинация начинается с нуля?
n2o.engine.pageStartsWith0=false
Чтобы получить общее количество записей можно использовать атрибут count-mapping
,
если REST сервис возвращает общее количество записей вместе со списком записей одной страницы.
<list result-mapping="['content']" count-mapping="['totalElements']">
<rest>/mystore/goods?size={limit}&page={page}</rest>
</list>
В атрибуте count-mapping
указывается путь к свойству, содержащему общее количество записей списка.
{
"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 не совпадает с идентификатором поля выборки, необходимо сделать маппинг.
<field id="firstName"
mapping="['first_name']"/>
Фильтрация данных
Для задания фильтров REST запроса нужно указать тип фильтра, идентификатор фильтра filter-id
и ссылку на поле выборки field-id
.
<fields>
<field id="id"/>
</fields>
<filters>
<eq field-id="id" filter-id="idEq"/> <!-- Плейсхолдер {idEq} -->
</filters>
В этом случае будет доступен плейсхолдер {idEq}
равный значению filter-id
.
Если плейсхолдер нужно переименовать, можно использовать маппинг.
<filters>
<eq field-id="gender.id"
filter-id="gender.id"
mapping="['gender_id']"/> <!-- Плейсхолдер {gender_id} -->
</filters>
Можно добавить специальный плейсхолдер {filters}
в REST запрос, чтобы шаблонизировать фильтрацию выборки.
<list filters-separator="&">
<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=Ноутбук
Обычно в REST сервисе заранее прошито как то или иное поле фильтруется на сервере.
В таком случае использование разных видов фильтров (<eq>
, <like>
, <in>
и др.) не оказывает влияение на запрос и является чисто семантическим.
Если ни одно значение фильтра не задано, плейсхолдер {filters}
будет заменен на пустую строку.
Сортировка данных
Сортировка записей в REST сервисах обычно задается в параметрах запроса.
<rest>/mystore/goods?sort=name,{nameDir}</rest>
В поле, поддерживающее сортировку, необходимо добавить атрибут sorting
и указать маппинг плейсхолдера для задания направления сортировки с помощью sorting-mapping
.
<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
аналогично.
Если ни одно направление сортировки не задано, плейсхолдер {sorting}
будет заменен на пустую строку.
Операции над данными
Для выполнения операций над данными нужно записать соответствующий REST запрос и http метод.
<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
.
<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
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 провайдер выполняет запросы.
@Bean
RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
... //настройки restTemplate
return restTemplate;
}
Получение данных
Для получения списка записей нужно выполнить запрос выборки данных к GraphQL сервису.
<list result-mapping="['data.goods']">
<graphql>
query myQuery() {
goods() {
id
name
}
}
</graphql>
</list>
В атрибуте result-mapping
нужно указать путь к списку объектов в ответе GraphQL сервиса
{
"data": {
"goods": [
{"id": 1, "name": "Товар 1"},
{"id": 1, "name": "Товар 2"},
...
]
}
}
Для получения одной записи по идентификатору в GraphQL сервисах обычно используется атрибуты метода.
Значение атрибута можно задать через плейсхолдер.
Плейсхолдеры задаются со знаком "двойной доллар" $$something
.
<unique filters="idEq" mapping="['data.goodById']">
<graphql>
query myQuery() {
goodById(id: $$idEq) {
id
name
}
}
</graphql>
</unique>
Пагинация данных
Для пагинации записей GraphQL запроса следует использовать плейсхолдеры $$size
и $$offset
.
<list>
<graphql>
query myQuery() {
goods(first: $$size, offset: $$offset) {
id
name
}
}
</graphql>
</list>
В качестве альтернативы $$offset
можно использовать плейсхолдер $$page
.
<list>
<graphql>
query myQuery() {
goods(size: $$size, page: $$page) {
id
name
}
}
</graphql>
</list>
Номер страницы $$page
по умолчанию начинается с нуля 0
, но можно начать нумерацию страниц с 1
задав это в настройке
#Паджинация начинается с нуля?
n2o.engine.pageStartsWith0=false
Чтобы получить общее количество записей можно использовать атрибут count-mapping
,
если 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
указывается путь к свойству, содержащему общее количество записей списка.
{
"data": {
"goods": [
{"id": 1, "name": "Товар 1"},
{"id": 1, "name": "Товар 2"},
...
],
"aggregateGoods": {
"count": 100
}
}
}
Маппинг полей
В результате выполнения GraphQL запроса вернется объект DataSet
.
К DataSet можно обращаться как к Map<String, Object>
, используя квадратные скобки ['field']
.
Для получения вложенных свойств используется символ "точка" ['field1.field2']
.
Если свойство json не совпадает с идентификатором поля выборки, необходимо сделать маппинг.
<field id="firstName"
mapping="['first_name']"/>
Фильтрация данных
Для задания фильтров GraphQL запроса нужно указать тип фильтра, идентификатор фильтра filter-id
и ссылку на поле выборки field-id
.
<fields>
<field id="id"/>
</fields>
<filters>
<eq field-id="id" filter-id="idEq"/> <!-- Плейсхолдер $$idEq -->
</filters>
В этом случае будет доступен плейсхолдер $$idEq
равный значению filter-id
.
Если плейсхолдер нужно переименовать, можно использовать маппинг.
<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
}
}
Обычно в GraphQL сервисе заранее прошито как то или иное поле фильтруется на сервере.
В таком случае использование разных видов фильтров (<eq>
, <like>
, <in>
и др.) не оказывает влияение на запрос и является чисто семантическим.
Если ни одно значение фильтра не задано, плейсхолдер $$filters
будет заменен на пустую строку.
Сортировка данных
Сортировка записей в GraphQL сервисах обычно задается в атрибутах методов.
<graphql>
query myQuery() {
goods(order: { $$nameDir: name } ) {
id
name
}
}
</graphql>
В поле, поддерживающее сортировку, необходимо добавить атрибут sorting
и указать маппинг плейсхолдера для задания направления сортировки с помощью sorting-mapping
.
<field id="name"
sorting="true"
sorting-mapping="['nameDir']"/>
<!-- Если будет сортировка по полю name
в плейсх олдер $$nameDir попадет значение asc или desc -->
Если 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
аналоги чно.
Если ни одно направление сортировки не задано, плейсхолдер $$sorting
будет заменен на пустую строку.
Операции над данными
Для выполнения операций над данными нужно задать 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
.
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. Либо можно вызвать статический метод класса.
<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>
<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 базе данных.
<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"/>
<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
.
<query>
...
<fields>
<field id="gender.id" domain="integer">
...
</field>
</fields>
</query>
<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
Стоит отметить, что для фильтров нет возможности использовать switch
К исходящим полям относятся:
- поля выборки
- <out> поля операции объекта
В этом случае, наоборот, маппинг является начальной операцией
mapping
-> default
-> normalize
-> switch
Значение по умолчанию
Маппинг данных в провайдерах
Входные и выходны е параметры провайдера могут несоответствовать полям ввода.
Для их приведения в соответствие используется атрибут mapping
.
Выражение в mapping
записывается на языке SpEL.
Провайдеры используют тип входных параметров: "ключ значение".
Java провайдеры в качестве альтернативы могут использовать тип входных параметров "массив значений".
Поэтому в маппинге java можно также обращаться по индексу аргумента, например, [0]
, [1].name
.
Маппинг фильтров
Маппинг фильтров в sql, rest, graphql и mongodb
<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>
<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>
<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>
<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
<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>
<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
POST /api/myentity
{
"firstName" : "John",
"genderId" : 1
}
<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 класса с примитивным типом аргументов:
package com.example;
class Calculator {
public static Long sum(Long a, Long b) {
return a + b;
}
}
Чтобы смапить значение поля ввода в примитивный аргумент 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>
Аргументы должны быть указаны в том же порядке, что и соответствующие аргументы java метода
Маппинг сущности
@Service
class MyService {
public Long create(MyEntity entity) { ... }
}
class MyEntity {
private String name;
private String surname;
//getters and setters
}
Тип entity
может быть задан только один раз среди всех аргументов.
<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 для задания сортировок и паджинации |
@Service
class MyService {
public List<MyEntity> getList(MyCriteria criteria) { ... }
}
class MyCriteria extends Criteria {
private Date birtdayBefore;
private Date birtdayAfter;
//getters and setters
}
Тип criteria
может быть задан только один раз среди всех аргументов.
Маппинга сортировки и паджинации не предусмотрено, они передаются через базовый класс наследник.
<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
SELECT name as fname, surname as lname FROM mytable
<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
query persons() {name age}
<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
GET /api/myentity/items
{
"data" : [
{
"name" : "John",
"surname" : "Doe"
},
...
],
"cnt" : 123
}
<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
@Repository
interface MyRepository extends JpaRepository<MyEntity, Long> {
Page<MyEntity> findAll();
}
class MyEntity {
private String name;
private String surname;
//getters and setters
}
<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>
<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
<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
POST /api/myentity
{
"result" : 123
}
<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>
:
<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
@Entity
class MyEntity {
@ManyToOne
private Gender gender;
@OneToOne
private Address addr;
//getters and setters
}
<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>
<!-- обратите внимание, что класс сущности может быть указан во внешнем файле -->
<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>
Описывать поля можно внутри составного поля (reference, list или set) или во внешнем файле. Однако, если вы опишите поля в обоих местах, то более приоритетным будет вариант задания полей внутри составного поля. Все поля будут взяты из него, а для полей присутствующих в обоих файлах (т.е. с совпадающим id) будет произведено слияние в пользу текущего объекта.
<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
}
<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>
Нормализацию также можно применять на данные, полученные выборкой в ответе от провайдера.
Для записи нормализующего выражения в тегах <list>
и <unique>
используется атрибут result-normalize
.
Ключевые слова
#this
Позволяет ссылаться на значение текущего поля, полученное после маппинга
<fields>
<field id="name" normalize="#this != null ? #this : 'default'"/>
</fields>
#data
Позволяет ссылаться на значение другого поля (на том же уровне вложенности), полученного после маппинга
<fields>
<field id="beginDate"/>
<field id="endDate"/>
<field id="period" normalize="#data['beginDate'] + ' - ' + #data['endDate']"/>
</fields>
#parent
Позволяет ссылаться на значение другого поля (на уровень выше), полученного после маппинга
<fields>
<field id="name"/>
<reference id="org">
<field id="fullName" normalize="#parent['name'] + '_' + #this"/>
</reference>
</fields>
Вызовы функций нормализации
В выражениях нормализации могут быть использованы:
- Статические java-функции
<fields>
<field id="max" normalize="T(java.lang.Math).max(1000, #this)"/>
</fields>
- Нестатические java-функции
<fields>
<field id="random" normalize="new java.util.Random().nextInt(100)"/>
</fields>
- Пользовательские классы
<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>
Аннотацией @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 к виду:
{
"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
, которая достаточно естественным образом решает подобные случаи.
<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>
является хорошей альтернативой нормализации,
но они также могут работать и в связке друг с другом.