Построение RESTful web API в Caché

  • Tutorial
В InterSystems Caché появилась поддержка REST. О том, что такое REST на Хабре уже писали и не раз. Если кратко — это паттерн построения RESTful web API, и ему присущи следующие свойства:
  • разделение клиента и сервера,
  • независимость от состояния (stateless),
  • кэшируемая и многоуровневая архитектура,
  • единый интерфейс,
  • все запросы к RESTful web API состоят из корневого URL приложения плюс частные подзапросы,
  • CRUD через HTTP — HTTP методы GET, PUT, POST, DELETE (RESTful web API).
Типичное REST-приложение выглядит примерно так: есть корневой URL (http://example.com/resources/) и дочерние URL (http://example.com/resources/item17), к которым мы обращаемся по HTTP, с помощью с методов GET, PUT, POST, DELETE. Ниже таблица методов и действий с одним элементом и коллекциями:
Метод Запросы к коллекции элементов
example.com/resources
Запросы к одному элементу
example.com/resources/itemID
GET Получить список URI элементов коллекции, возможно доп. информацию Получить всю информацию об элементе
PUT Заменить существующую коллекцию на новую Заменить существующий элемент на новый
POST Создать новый элемент коллекции Как правило не используется
DELETE Удалить всю коллекцию Удалить элемент коллекции

А как в Caché?


В СУБД Caché поддержка REST появляется начиная с версии 2014.1 — эта версия пока доступна в виде филд-тест версии для партнеров и вузов InterSystems Campus. Чтобы создать REST приложение, нужно в настройках веб-приложения Caché определить класс-брокер, в котором указываются возможные расширения базового URL и соответствующие действия приложения при запросе этих расширений.
Класс-брокер создается как наследник класса %CSP.REST. Далее в нем прописывается карта путей URL приложения (например example.com/resources/ID — GET ), причем каждому URL в соответствие ставится метод класса Caché, который будет выполнять всю работу.
Карта путей — это перечисление всех возможных URL для обращения к приложению для получения данных или для изменения данных на сервере.

Примерная блок-схема работы REST веб приложения в Caché


За дело


Для начала создадим веб-приложение /rest с Dispatch Class REST.Broker
  1. В Портале Управления Системой (Портал):
    • Портал → Администрирование системы → Безопасность → Приложения → Веб приложения
    • Нажмите кнопку «Создать новое веб приложение»

  2. На странице редактирования веб приложения нужно заполнить следующие поля (остальные поля остаются без изменений):
    • Имя: /rest (слеш обязателен)
    • Область: USER
    • Dispatch Class: REST.Broker (строка регистрозависима)

  3. Нажмите кнопку Сохранить



Теперь надо создать класс REST.Broker - карту путей будущего web API. Открываем Studio
  1. Перейдите в область USER
  2. Создайте новый класс REST.Broker, нажав Ctrl+N или из Меню: Файл→Новый.
  3. Выберите вкладку Общие а там Класс Caché
  4. В мастере создания класса:
    • Введите имя пакета: REST
    • Укажите имя класса: Broker
    • Нажмите кнопку Далее

  5. В окне Тип класса:
    • Нажмите на кнопку Расширения
    • Имя класса-предка: %CSP.REST
    • Нажмите кнопку Завершить

Прописываем в классе-брокере карту путей и метод-обработчик
Class REST.Broker Extends %CSP.REST
{
XData UrlMap
{
<
Routes>
 <
Route Url="/test" Method="GET" Call="Test"/>
 </
Routes>
}
ClassMethod Test() As %Status
{
    
&html<Работает!>
    
quit $$$OK
}
}


В карте путей XData UrlMap при доступе к URL /test происходит вызов метода Test класса REST.Broker. В случае вызова методов других классов, в Call нужно также указывать имя класса

По адресу http://<адрес сервера>/rest/test должна выводиться надпись «Работает!» Пример
Простейшее RESTful web API готово.

Подготовка данных


Для более сложного примера нам понадобятся данные. Создадим класс Data.Company
  1. В мастере создания класса:
    • Имя пакета: Data
    • Имя класса: Company
    • Нажмите кнопку Далее.

  2. В окне Тип класса:
    • Нажмите на кнопку Persistent
    • Нажмите кнопку Далее
    • Выберите опцию XML Enabled
    • Выберите опцию Data Population (Генерация тестовых данных)

  3. Нажмите кнопку Завершить

Создадим свойство Name - у каждой компании должно быть название
Class Data.Company Extends (%Persistent%Populate%XML.Adaptor)
{
Property Name As %String(POPSPEC "Company()");
}


POPSPEC "Company()" — сообщаем генератору тестовых данных, чего мы от него хотим, а то он бы нам имена людей выдавал тут.

Заполним класс тестовыми данными с помощью команды в терминале: ##class(Data.Company).Populate(10)

На стороне сервера


Для демонстрации CRUD операции Return (HTTP — GET) мы создадим новый класс REST.JSON для задач генерации JSON ответов на запросы к RESTful сервису.

Создаём класс REST.JSON
  1. В мастере создания класса:
    • Имя пакета: REST
    • Имя класса: JSON
    • Нажмите кнопку Далее.

  2. В окне Тип класса:
    • Нажмите на кнопку Расширения
    • Имя класса-предка: %Base

  3. Нажмите кнопку Завершить

Для начала напишем метод, отдающий JSON, содержащий список компаний со всеми их свойствами
ClassMethod GetAllCompanies() As %Status
{
    
   
set st=$$$OK
   try 
{    
   
do ##class(%ZEN.Auxiliary.jsonSQLProvider).%WriteJSONFromSQL(,"select * from Data.Company")
   
catch ex {
       
set st=ex.AsStatus()
   
}
   
quit st
}

Интересен здесь разве что метод класса jsonSQLProvider — он выводит на текущее устройство результат SQL запроса в формате JSON. Обратите внимание на запятую в списке параметров, она обязательна, т.к. первый параметр – опциональное имя javascript переменной на стороне клиента.

Отдавать данные в JSON мы научились, однако брокер об этом не знает.

Добавим в карту путей REST.Broker путь, чтобы знал
Route Url="/json/companies" Method="GET" Call="REST.JSON:GetAllCompanies"/>

Готово! Теперь по адресу http://<адрес сервера>/rest/json/companies вас ждёт список компаний в JSON, пример.

Серверу — клиента!


А на стороне клиента мы будем превращать JSON, во что-нибудь, приятное глазу. Для этого используем MVC JS-фреймворк AngularJS.

Но для начала создадим новую CSP страницу
  1. В Caché Studio, создайте новую CSP страницу, нажав Ctrl+N или из Меню: Файл→Новый
  2. Выберите вкладку CSP файл
  3. Выберите Caché Server Page и нажмите кнопку OK
  4. Сохраните созданную страницу в папке csp/user под именем rest.csp

Делаем запрос к серверу, получаем ответ - список компаний в JSON - отображаем его
<!doctype html>
<html ng-app>
<
head>
<
title>REST Academy</title>
<
script src="ajax.googleapis.com/ajax/libs/angularjs/1.2.3/angular.min.js"></script>
<
script language="javascript">
function ctrl($scope,$http) {
    
// Запрос GET к RESTful web API
   
$http.get("/rest/json/companies").success(function(data) {
        
// Помещаем ответ сервера в переменную companies
       
$scope.companies=data.children;
   }).error(
function(data, status) {
        
// Вывод информации об ошибке, если таковая возникнет
       
alert("["+status+"]   Ошибка при загрузки компаний!["+data+"]");
   })
};
</
script>
</
head>
<
body ng-controller="ctrl">
    <
div ng-repeat="company in companies">
        {{company.Name}}
    </
div>
</
body>
</
html>

Компилируем страницу, переходим по адресу http://<адрес сервера>/csp/user/rest.csp и смотрим на полный список компаний. Как и раньше, пример.

Реализация Create, Update, Delete на сервере


Реализуем серверную бизнес-логику для оставшихся 3 операций CRUD: добавление, изменение и удаление компании.

Для этого в класс REST.JSON добавляем методы
ClassMethod CreateCompany() As %Status
{
    
st=$$$OK
    try 
{
    
// Берём JSON из запроса и конвертируем в объект класса Data.Company
    
$$$THROWONERROR(st,##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(%request.Content,"Data.Company",.obj,1))
    
$$$THROWONERROR(st,obj.%Save())
    

    
catch ex {
        
st=ex.AsStatus()
    
}
    
quit st
}
ClassMethod DeleteCompany(compid As %StringAs %Status
{
    
set st=$$$OK
    try 
{
        
$$$THROWONERROR(st,##class(Data.Company).%DeleteId(compid))
    
catch ex {
        
st=ex.AsStatus()
    
}
    
quit st
}
ClassMethod UpdateCompany(compid As %StringAs %Status
{
 
set st=$$$OK
 try 
{
   
   
$$$THROWONERROR(st,##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(%request.Content,,.obj,1))
   
   
// Открываем объект, который хотим отредакнировать
   
set comp=##class(Data.Company).%OpenId(compid)
   
throw:comp=$$$NULLOREF ##class(%Exception.StatusException).CreateFromStatus($$$ERROR(5001,"Company does not exist"))
    
// Редактируем и сохраняем
   
set comp.Name=obj.Name
   $$$THROWONERROR
(st,comp.%Save())
 

 
catch ex {
   
set st=ex.AsStatus()
 
}
 
quit st
}

Добавляем соответствующие пути в брокер
<Route Url="/json/company" Method="POST" Call="REST.JSON:CreateCompany"/> 
<
Route Url="/json/company/:compid" Method="DELETE" Call="REST.JSON:DeleteCompany"/>
<
Route Url="/json/company/:compid" Method="PUT" Call="REST.JSON:UpdateCompany"/>

На этом завершается создание CRUD-полного RESTful web API в Caché.

Клиентская реализация Create, Update, Delete


Добавляем странице rest.csp функциональность по созданию, изменению, удалению компаний
<!doctype html>
<html ng-app>
<head>
<title>REST Academy</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.3/angular.min.js"></script>
<script language="javascript">
function ctrl($scope,$http) {
    // Запрос GET к RESTful web API
        $scope.getCompanies=function() {
       $http.get("/rest/json/companies").success(function(data) {
             // Помещаем ответ сервера в переменную companies
           $scope.companies=data.children;
       }).error(function(data, status) {
                 // Вывод информации об ошибке, если таковая возникнет
           alert("["+status+"] Ошибка при загрузке компаний! ["+data+"]");
       });
   };
  
      // Создать новую компанию
    $scope.create = function (company){
       $http.post("/rest/json/company",company)
       .success(function(data){$scope.getCompanies();$scope.alertzone="Добавили компанию "+company.Name;}).error(function(data,status){
        $scope.alertzone="["+status+"] Ошибка добавления компании :( ["+data+"]"; });
    }

    // Обновить существующую компанию
  $scope.update = function (company){
       $http.put("/rest/json/company/"+company.ID,company)
        .success(function(data){$scope.alertzone="Обновили компанию "+company.Name;}).error(function(data,status){ // поменял alert(....); на alertzone
        $scope.alertzone="["+status+"] Ошибка обновления имени компании :( ["+data+"]"; });
    }
            
    // Удалить компанию
    $scope.delete = function (company){
        $http.delete("/rest/json/company/"+company.ID)
        .success(function(data){$scope.getCompanies();$scope.alertzone="Удалили компанию "+company.Name;}).error(function(data,status){
            $scope.alertzone="["+status+"] Ошибка удаления компании :( ["+data+"]"; });
    }
};
</script>
</head>
<body ng-controller="ctrl" ng-init="getCompanies();">

<h4 ng-model="alertzone"><font color=red>{{alertzone}}</font></h4>

<form name="compCreateForm" ng-model="company" ng-submit="create(company); company='';">
    Добавить компанию <input type="text" ng-model="company.Name"/>
    <input type="submit" value="Добавить"/>
</form>
<br>
<div ng-repeat="company in companies">
    <form name="compForm" ng-submit="update(company); compForm.$setPristine();">
        <input type="text" ng-model="company.Name"/>
        <input type="submit" value="Сохранить" ng-show="compForm.$dirty"/>
        <input type="button" value="X" ng-click="delete(company);"/>
    </form>
</div>
</body>
</html>


В результате готов фронтэнд к web API по адресу http://<адрес сервера>/csp/user/rest.csp. Пример.

Итого


В рамках данной статьи мы научились строить и настраивать RESTful web API на сервере Caché. Построение клиентской части также возможно на основе сервера Caché.

Что дальше?


Если кому-то интересно могу рассказать про обеспечение безопасности, разделение прав, и другие полезности для разработки RESTful web API на базе Caché.

Полезные ссылки


Скачать RESTful web API, построенное в этом туториале
Пример RESTful web API
Глоссарий по технологиям InterSystems — так же RESTful web API
JSON экспорт — класс класс2(SQL)
%request класс
%responseкласс
XML экспорткласс

InterSystems

76,00

Вендор: СУБД Caché, OLAP DeepSee, шина Ensemble

Поделиться публикацией

Похожие публикации

Комментарии 15
    0
    Есть ли у вас пакетные операции, например удалить несколько ID? Как вы их реализуете, начиная от вида uri, http метода запроса и параметров передаваемых клиентом, заканчивая серверной логикой. Специальный ли это обработчик или это все сводится к обработчику удаления одного ID, как проверяется авторизация и права на выполнение операции?
      –1
      Есть ли у вас пакетные операции, например удалить несколько ID?

      Стараемся обходиться без них, как не вполне соответствующим принципам REST. Но в случае реализации, передавали бы массивом список ID. На стороне сервера будет обработчик удаления одного ID который проверяет право пользователя на совершение действия перед вызовом метода удаления одного ID, т.к. иначе и парсинг, проверка прав и работа с базой была бы в одном методе.

      как проверяется авторизация и права на выполнение операции?

      Вот тут предложено несколько методов разграничения прав пользователей в RESTful web API. Планирую написать продолжение статьи как раз об этом вопросе.
        0
        HTTP метод при этом в запросе остается DELETE? Параметры передаются в строке URI после "?" (как с ограничением длины дела обстоят)?
          0
          HTTP метод при этом в запросе остается DELETE?

          Да. Хотя на стороне Caché легко можно добавить свои методы вроде BATCHDELETE.
          Вот так
          В классе %Projection.REST в блоке <xs:simpleType name="action"
          Добавьте XML нод: <xs:enumeration value="BATCHDELETE" />
          Скомпилируйте класс %Projection.REST
          Добавьте в карту путей брокера новый путь: <Route Url="/test3/:ArrayArg" Method="BATCHDELETE" Call="Test3">
          Скомпилируйте класс брокера.

          Либо можно попробовать модифицировать карту путей так, чтобы при передаче одного элемента и нескольких элементов с разделителем вызывались различные методы.

          Параметры передаются в строке URI после "?" (

          Параметры передаются так: server.com/rest/collection/arg1/arg2/arg3

          как с ограничением длины дела обстоят?

          На стороне сервера происходит Regex поиск совпадения, так что теоретически всё ограничено максимальной длиной строки в Caché (3641144 в 2014.1), однако существуют ещё и ограничения браузера/веб-сервера.
            0
            Либо можно попробовать модифицировать карту путей так, чтобы при передаче одного элемента и нескольких элементов с разделителем вызывались различные методы.

            Ещё можно добавить в метод проверку на существования параметра — для более гибкого вывода.
            Например: добавить параметр id в методе GetAllCompanies(id As %String) As %Status. В брокере будет запись вида:
            URL = "/json/companies/:id"
            тогда добавим в метод условие проверки добавляющее в запрос строку " Where id = '"_id_"'" (не забудьте перед where поставить пробел) — получим конкретную компанию, а в случае его отсутствия запрос выдаст все компании.
              0
              " Where id = '"_id_"'"

              Не надо советовать людям, то как не надо делать, пускай sql-инъекции в Cache сложнее, но они все равно возможны.
              И советую вам тоже так не делать.
                0
                Защита от инъекций реализуется в DispatchRequest после этого в классы приходят только проверенные данные, к тому же когда таких запросов много текст брокера и обработчиков воспринимается легче. Безопасность вне Caché security — тема для отдельной статьи.
                  0
                  Всегда есть вероятность что мимо проверки что-то может проскочить, а вот при использовании параметризованных запросов, защита уже 100%. И не стоит этим пренебрегать никогда.
                    0
                    «параметризованных» и «защита 100%»
                    Это как? Спасибо.
                    мимо проверки
                    На мой взгляд одно из удобств связки REST и брокера состоит в том что мы получаем единую точку передачи параметров в базу и вызова методов, которую вполне возможно контролировать.
                      0
                      Параметризованные запросы, это когда все необходимые параметры для фильтрации запроса передаются в Execute или другой подобный метод, как отдельные переменные, а не формируется sql на их основе. Защита 100%, это значит что при таком формировании запросов, никакие значения входящих параметров никак не испортят сам запрос.

                      Не всегда есть возможность построить приложение таким образом чтобы все запросы от пользователя шли только через брокер. Например, если приложение старое и только переходит на новые интерфейсы, и было написано иначе, либо вообще не используется REST и брокер. Поэтому нужно всегда следовать некоторым правилам при разработке, так будто на любом этапе данные могут быть испорчены. Дополнительные проверки не помешают и добавят безопасности вашему приложению.
        0
        Как вы их реализуете

        Зависит от того кто и как формирует JSON для передачи в Caché. На стороне сервера, в приёмнике необходимо приведении типов. Например почти всегда необходимо указывать в %ConvertJSONToObject тип обрабатываемого объекта, здесь это "Data.Company".
        Специальный ли это обработчик

        Да. В большинстве случаев необходимо писать свой обработчик. Мы формируем массив данных c помощью AngularJS и передаём их как в примере. Так же в JSON можно передавать несколько наборов данных.
          0
          авторизация и права

          Если используется CSP (Caché Server Pages), то достаточно встроенной системы безопасности Так же Caché умеет создавать cookies на сервере.
          0
          А какие есть альтернативы построения REST API для свободных баз данных?
            0
            Не являюсь специалистом по FOSS СУБД, но мне кажется более вероятным использование каких-либо внешних средств для построения RESTful web API с подключением их к бд.

            Также можно скачать однопользовательскую версию Cache (бесплатно) или использовать GlobalsDB с NodeJS.
            0
            Поставил минус за смешение в аннотации понятий REST и RESTful.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое