Пример реализации универсального REST сервиса (Avalanche — application framework for Java)
"Avalanche — application framework for Java" — реализация технологии стирающей различия
между вызовами локального и удаленного кода. Отказоустойчивость, масштабируемость,
модифицируемость, непрерывная доступность идут в комплекте приятными бонусами.
В статье описан пример реализации универсального REST сервиса. В основу идеи реализации сервиса положено утверждение, что FrontEnd в каждый момент времени "знает", какими данными он манипулирует и что собирается запросить у BackEnd-а. Это утверждение позволило отказаться от разработки ORM в BackEnd-е и использования JPA при реализации универсального сервиса. Сервис способен манипулировать содержимым любой таблицы БД. Изменения структуры или созданные объекты БД сразу становятся доступны методам сервиса.
В сервисе реализованы методы трех групп:
- .../data/table/… — манипулирования содержимым таблиц БД;
- .../data/query/… — выполнение произвольных запросов, расширяющих функциональность сервиса;
- .../data/info/… — получения информации о структуре объектов БД.
В сервисе реализована система ошибок, которая однозначно позволяет понять причину и место возникновении ошибки. По умолчанию, поддерживается локализация сообщений ru и en. Не все сообщения могут быть локализованы, например сообщения ошибок выполнения SQL запросов, которые зависят от локализации среды выполнения.
Сервис поддерживает работу с данными в форматах XML и JSON.
Сервис реализован с использованием библиотеки Jersey 2.0.
В общем виде формат URL строки запроса сервиса выглядит так
http://host:port/name_context/map_servlet/data/type_group/{object}[?request_parameters]
[request_body]
Где:
- name_context — имя контекста (WEB приложения);
- map_servlet — имя маппирования сервлета Jersey в файле web.xml;
- data — путь этого сервиса
- type_group — тип группы запросов (table, query или info);
- {object} — параметр запроса
Параметр запроса {object} может принимать значения:
- для типа table — имя_схемы.имя_таблицы в формате schema.table;
- для типа query — имя_класса.имя_метода в формате class.method, имя класса указывается без пакета, поиск класса осуществляется в пакетах, указанных в параметре packages;
- для типа info — имя_метода в формате method;
request_parameters — параметры запроса, могут отсутствовать
request_body — тело запроса, может отсутствовать
В HTTP заголовке могут указываться параметры:
- Accept — не обязательный, формат данных, ожидаемый в ответе на запрос, может принимать значения application/json (по умолчанию) или application/xml;
- Content-Language — не обязательный, язык локализации, может принимать значения ru (по умолчанию) или en;
- Content-Type — обязательный, при наличии тела запроса, может принимать значения application/json или application/xml.
Реализация сервиса
Реализация сервиса TableService.java опубликована на GitHub.
Пример конфигурационного файла сервиса avalanche-restdb-config.xml
Пример конфигурационного файла контекста restdb.xml
Ниже приведены примеры различных запросов
Примеры запросов
Пример получения записи таблицы rp.structure_type по значению первичного ключа
Запрос:
GET /restdb/rs/data/table/rp.structure_type?st_id=43 HTTP/1.1
Accept: application/json
Content-Language: ru
Host: localhost:8080
Ответ: (HTTP Status — 200)
[
{
"st_id": 43,
"st_name": "Test43 "
}
]
Пример получения нескольких записей таблицы rp.structure_type по значению первичного ключа
Запрос:
GET /restdb/rs/data/table/rp.structure_type?st_id=43&st_id=42 HTTP/1.1
Accept: application/json
Content-Language: ru
Host: localhost:8080
Ответ: (HTTP Status — 200)
[
{
"st_id": 42,
"st_name": "Test42 "
},
{
"st_id": 43,
"st_name": "Test43 "
}
]
Пример получения всех записей таблицы rp.users, если это не запрещено в конфигурации сервиса
Запрос:
GET /restdb/rs/data/table/rp.users HTTP/1.1
Accept: application/json
Content-Language: ru
Host: localhost:8080
Ответ: (HTTP Status — 200)
[
{
"us_last": "Петров ",
"us_name": "Петр ",
"us_email": "p_petrov@domain.ru "
},
{
"us_last": "Сидоров ",
"us_name": "Сидор ",
"us_email": "s_sidorov@domain.ru "
},
{
"us_last": "Пупкин ",
"us_name": "Пуп ",
"us_email": "p_pupkin@domain.ru "
},
{
"us_last": "Остапов ",
"us_name": "Остап ",
"us_email": "o.ostapov@domain.ru "
},
{
"us_last": "Иванов ",
"us_name": "Иван ",
"us_email": "i.ivanov@domain.ru "
},
{
"us_last": "Кириллов ",
"us_name": "Кирилл ",
"us_email": "k.kirillov@domain.ru "
}
]
Пример получения записей таблицы rp.users, имеющих вхождение подстроки "ров" в поле us_last
Запрос:
GET /restdb/rs/data/table/rp.users?us_name=%ров% HTTP/1.1
Accept: application/json
Content-Language: ru
Host: localhost:8080
Ответ: (HTTP Status — 200)
[
{
"us_last": "Петров ",
"us_name": "Петр ",
"us_email": "p_petrov@domain.ru "
},
{
"us_last": "Сидоров ",
"us_name": "Сидор ",
"us_email": "s_sidorov@domain.ru "
}
]
Пример вставить одну запись в таблицу rp.test
Если в теле запроса будет указано несколько записей вставятся в таблицу множество записей.
Запрос:
POST /restdb/rs/data/table/rp.test HTTP/1.1
Accept: application/json
Content-Language: ru
Content-Length: 58
Host: localhost:8080
Content-Type: application/json
[{"ts_id":9, "ts_tszone":"2019-01-19T15:37:13.380+03:00"}]
Ответ: (HTTP Status — 200)
{
"query": "INSERT",
"result": 1,
"timer": 3
}
Пример вставить одну запись в таблицу rp.test
Пример, аналогичный запросу выше, но с указанием параметров вставляемой записи в строке запроса. Если в строке запроса присутствуют параметры то тело запроса в HTTP методе POST игнорируется.
Запрос:
POST /restdb/rs/data/table/rp.test?ts_id=10&ts_tszone=2019-01-19T15:37:13.380+03:00 HTTP/1.1
Accept: application/json
Content-Language: ru
Content-Length: 58
Host: localhost:8080
Content-Type: application/json
Ответ: (HTTP Status — 200)
{
"query": "INSERT",
"result": 1,
"timer": 3
}
Пример неудачной попытки вставить запись с использование метода insert класса SQL генератора TestQuery
В теле запроса допущена опечатка (намеренно), в имени поля us_last пропущена буква "s"
Запрос:
POST /restdb/rs/data/query/TestQuery.insert HTTP/1.1
Accept: application/xml
Content-Language: ru
Content-Length: 61
Host: localhost:8080
Content-Type: application/json
[{"us_name":"3", "us_lat":"333", "us_email":"333@domain.ru"}]
Ответ: (HTTP Status — 400)
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<serverError>
<cause>
[24344@likhovskikh-vv] SYS0250E При вызове метода "execute" возникла ошибка.
Ошибка: "ru.funsys.avalanche.sql.SQLException: [24344@likhovskikh-vv] SQL0021E При выполнении запроса произошла ошибка. Номер запроса: 0, запрос: INSERT INTO rp.users (us_name, us_last, us_email) VALUES (?, ?, ?).
org.postgresql.util.PSQLException: ОШИБКА: нулевое значение в столбце "us_last" нарушает ограничение NOT NULL
Подробности: Ошибочная строка содержит (3 , null, 333@domain.ru )."
</cause>
<cоde>RST0007E</cоde>
<message>При выполнение SQL запроса произошла ошибка.</message>
</serverError>
Преимущества универсального сервиса по сравнению с общепринятым подходом реализации REST сервисов.
- Универсальный REST сервис готов для использования с любой реляционной БД.
- Нет зависимости от требования JPA обязательного наличия индексного идентификатора ID в каждой таблице модели данных. Коллеги, в реляционной теории БД нет понятия индексного поля таблицы ID, которое используется в качестве первичного ключа. Наличия этого поля превращает вашу реляционную БД в индексную.
- URI, параметры запроса, тело запроса и результат выполнения запроса формализованы.
- Универсальный сервис легко расширяемый, достаточно определить требуемые методы генерации SQL запросов в одном или нескольких классах SQL генераторов.
- Первичный ключ таблиц БД может состоять из множества полей. Поля первичного ключа доступны для модификации при соблюдении условий целостности.
- Отсутствие отображения программной модели объектов модели данных в БД значительно упрощает эксплуатацию, особенно в распределенной среде (например, в системах с георезервированием во множестве ЦОД).