Пример реализации универсального 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 генераторов.
- Первичный ключ таблиц БД может состоять из множества полей. Поля первичного ключа доступны для модификации при соблюдении условий целостности.
- Отсутствие отображения программной модели объектов модели данных в БД значительно упрощает эксплуатацию, особенно в распределенной среде (например, в системах с георезервированием во множестве ЦОД).
