Провайдеры данных
В 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>