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