Одновременный сеанс в IRIS: SQL, объекты, REST, GraphQL

Казимир Малевич, Спортсмены (1932)

«И вполне естественно ваше непонимание. Разве может понять человек, который ездит всегда в таратайке, переживания и впечатления едущего экспрессом или летящего в воздухе»

Казимир Малевич (1916)

Как то мы уже обращались к теме превосходства объектного/типизированного представления в реализации моделей предметной области в сравнении с SQL. И верность тех доводов и фактов ни на йоту не уменьшилась. Казалось бы, зачем отступать и обсуждать технологии, которые глобально низвергают абстракции обратно в дообъектную и дотипизированную эпоху? Зачем провоцировать рост спагетти-кода, непроверяемых ошибок и упование на виртуозное мастерство разработчика?

Есть несколько соображений о том, почему стоит поговорить про обмен данными через API на основе SQL/REST/GraphQL, в противовес представлению их в виде типов/объектов:

  • это массово изучаемые и достаточно легко используемые технологии;

  • их широкая популярность в доступных и открытых программных продуктах просто невероятна;

  • часто, особенно в вебе и в базах данных, у вас просто нет выбора;

  • или наоборот, когда у вас всё ж таки есть выбор – его надо сделать осознанно:

  • и главное, внутри в границах этих API остаются объекты, как наиболее адекватный способ их реализации в коде.

Перед тем, как обозреть реализации API, взглянем для начала на уровни абстракции ниже. Взглянем на схему путешествия данных между местами их постоянного хранения, а также местами обработки и представления пользователю наших приложений.

Сегодня, данные хранятся, как давно повелось, на жёстких дисках в HDD или, уже по современному, в микросхемах флеш-памяти в SSD. Данные пишутся и читаются потоком, состоящем из отдельных блоков хранения на HDD/SSD. 

Деление на блоки не случайное, а задаваемое физикой/механикой/электроникой накопителя данных. В HDD — это дорожки/сектора на вращающемся магнитном диске. В SSD — это сегменты памяти в перезаписываемом кремниевом чипе. Суть одна — это блоки информации, из частей которых необходимо найти и собрать воедино нужные нам кусочки данных. Собрать в структуры, которые соответствуют нашей модели/типу данных со значением необходимым на момент запроса. За этот процесс как раз отвечает связка из СУБД и файловой подсистемы в операционной системе.

По правде говоря, можно обращаться и минуя СУБД — напрямую к файловой системе или даже напрямую к HDD/SSD. Но тогда мы теряем два супер важных мостика к данным – первый, между блоками хранения и файловыми потоками, и, второй, между файлами и упорядоченной структурой в модели базы данных. Или, говоря другими словами, мы берём на себя ответственность по разработке всего этого объёма кода для обработки блоков/файлов/моделей со всеми оптимизациями, тщательной отладкой и долговременными испытаниями на надёжность.

Так вот, СУБД дают нам прекрасную возможность обращаться с данными на языке высокого уровня сразу оперируя понятными моделями и представлениями. Хвала им за это. А хорошие СУБД и платформы данных, такие как InterSystems IRIS, предоставляют больше — доступ к упорядоченным данным сразу множеством способов одновременно. И выбор уже за программистом, какой из них выбрать для своего проекта.

Так воспользуемся же множественными привилегиями, которые даёт нам IRIS. Сделаем код красивее и полезнее — сразу будем применять объектно-ориентированные язык ObjectScript для использования и разработки API. То есть, например, SQL код будем вызывать прямо изнутри программы на ObjectScript. А для других API воспользуемся готовыми библиотеками и встроенными средствами ObjectScript.

Для примеров будем использовать данные из замечательного интернет-проекта "SQL Zoo", в котором обучают языку запросов SQL. Эти же данные будем использовать в других примерах API.

Если хочется сразу посмотреть на многообразие подходов к проектированию API и воспользоваться готовыми решениями, то вот интересная и полезная коллекция публичных API, которая коллективно собирается в проекте на гитхабе https://github.com/public-apis/public-apis

SQL

Начинаем вполне естественно с SQL. Кто ж его не знает? 

Для изучения SQL есть гигантское количество учебных курсов и книг. Мы будем опираться на https://sqlzoo.net. Это хороший начальный онлайн курс по SQL с примерами, прохождением заданий и справочником языка. 

Будем переносить задачки из SQLZoo на платформу IRIS и получать аналогичные решения разными способами.  

Как быстро получить доступ к InterSystems IRIS на своём компьютере? Один из самых быстрых вариантов — развернуть контейнер в докере из готового образа InterSystems IRIS Community Edition — бесплатная версия InterSystems IRIS Data Platform

Другие способы получить доступ к InterSystems IRIS Community Edition на портале обучения.

Перенесём данные с SQLZoo в хранилище нашего собственного экземпляра IRIS. Для этого:

  1. Открываем портал управления (у меня, например, по ссылке http://localhost:52773/csp/sys/UtilHome.csp), 

  2. Переключаемся на область USER - в Namespace %SYS  нажимаем ссылку Switch и выбираем USER

  3. Переходим в меню Система > SQL - открываем Обозреватель системы, затем SQL и жмём кнопку Запустить.

  4. Справа на будет открыта закладка "Исполнить запрос" с кнопкой "Исполнить" - она то нам и нужна.

Более подробно о работе с SQL через портал управления можно посмотреть в документации.

Кстати, другим, удобным вам, способом попробовать SQL доступ к базе данных в IRIS, может оказаться популярный у разработчиков редактор Visual Studio Code с плагином SQLTools и драйвером "SQLTools Driver for InterSystems IRIS". Попробуйте этот вариант.

Инструкция – как настроить доступ к IRIS и разработке на ObjectScript в VSCode.

Готовые скрипты для развёртывания базы данных и набора тестовых данных SQLZoo есть в описании на сайте в разделе Данные. 

Пара прямых ссылок для таблицы World:

Скрипт для создания базы данных можно выполнить тут же на портале управления IRIS в форме "Исполнитель запросов".

CREATE TABLE world(

   name VARCHAR(50) NOT NULL

   ,continent VARCHAR(60)

   ,area DECIMAL(10)

   ,population DECIMAL(11)

   ,gdp DECIMAL(14)

   ,capital VARCHAR(60)

   ,tld VARCHAR(5)

   ,flag VARCHAR(255)

   ,PRIMARY KEY (name)

)

Для загрузки набора тестовых над формой "Исполнитель запросов" есть меню Мастера > Импорт данных. Для этого каталог с файлом тестовых данных необходимо добавить заранее, при создании вашего контейнера, или загрузить с вашего компьютера через браузер — такая опция есть тут же в мастере импорта данных панели управления.

Проверяем наличие таблицы с данными запуском простейшего скрипта в форме "Исполнитель запросов":

SELECT * FROM world

Теперь нам доступны примеры и задания с сайта "SQL Zoo". Все примеры ниже реализуют SQL запрос из первого задания:

SELECT population 

  FROM world 

 WHERE name = 'France'

Так что можете смело продолжать начатое нами исследование API перенося задания из SQLZoo на платформу IRIS.

Внимание! Как я обнаружил, данные в интерфейсе сайта SQLZoo и данные его экспорта отличаются. Как минимум в первом примере расходятся значения по населению Франции и Германии. Не переживайте. Вот данные Евростата для ориентировки.

Что бы плавно перейти к следующему шагу — объектный доступ к нашей базе данных, сделаем  небольшой промежуточный шаг от "чистых" SQL запросов к SQL запросам встроенные в код приложения на ObjectScript — объектно-ориентированном языке, встроенном в IRIS.

Class User.worldquery

{

ClassMethod WhereName(name As %String)

{

    &sql(

        SELECT population INTO :population

        FROM world 

        WHERE name = :name

    )

    

    IF SQLCODE<0 {WRITE "SQLCODE error ",SQLCODE," ",%msg  QUIT}

    ELSEIF SQLCODE=100 {WRITE "Query returns no results"  QUIT}

    WRITE name, " ", population

}

}

Проверяем результат в терминале:

do ##class(User.worldquery).WhereName("France")

В ответ должны вернуться название страны и число жителей.

Объекты/типы

Это общая преамбула к истории про REST/GraphQL. Мы реализуем API в сторону веб протоколов. Чаще всего у нас под капотом на серверной стороне будет исходный код на каком-то из языков, хорошо поддерживающих типизацию или даже целиком объектно-ориентированную парадигму. Вот те самые: Spring на Java/Kotlin, Django на Python, Rails на Ruby, ASP.NET на C# или даже Angular на TypeScript. И безусловно объекты в ObjectScript, родном для платформы IRIS. 

Почему это важно? Типы и объекты в вашем коде, при отправке вовне, будут упрощены до структур данных. Необходимо учитывать упрощение моделей в программе, что аналогично учёту потерь в реляционных моделях. И заботиться о том, что на другой стороне API, модели будут адекватно восстановлены и использованы вами или другими разработчиками без искажений. Это дополнительная нагрузка и увеличение ответственности программиста. Вне кода, вне помощи трансляторов/компиляторов и других автоматических инструментов при создании программ, необходимо вновь и вновь заботиться о корректной передаче моделей.

Если посмотреть на вопрос с другой стороны, то пока не видно на горизонте технологий и инструментов легко передающих типы/объекты из программы на одном языке в программу на другом. Что остаётся? Вот эти упрощения SQL/REST/GraphQL и разливанное море документации с описанием API на человечески понятном языке. Неформальная, с компьютерной точки зрения, документация для разработчиков – это как раз то, что следует всячески переводить в формальный код, который компьютер обрабатывать умеет.

Подходы к решению этих проблем предпринимаются регулярно. Одно из успешных – это кросс языковая парадигма в объектной СУБД платформы IRIS.

Что бы чётко понимать как соотносятся модели ОПП и SQL в IRIS, предлагаю посмотреть на таблицу:

Объектно-ориентированное программирование (ООП)

Структурированный язык запросов (SQL)

Пакет

Схема

Класс

Таблица

Свойство

Столбец

Метод

Хранимая процедура

Отношение между двумя классами

Ограничение внешнего ключа, встроенный join

Объект (в памяти или на диске)

Строка (на диске)

Более подробно про отображение объектной и реляционной модели можно посмотреть в документации IRIS.

При выполнении нашего SQL запроса на создание таблицы world из примера выше, IRIS автоматически генерирует у себя описания соответствующего объекта — класс с именем User.world.

Class User.world Extends %Persistent [ ClassType = persistent, DdlAllowed, Final, Owner = {_SYSTEM}, ProcedureBlock, SqlRowIdPrivate, SqlTableName = world ]

{

Property name As %Library.String(MAXLEN = 50) [ Required, SqlColumnNumber = 2 ];

Property continent As %Library.String(MAXLEN = 60) [ SqlColumnNumber = 3 ];

Property area As %Library.Numeric(MAXVAL = 9999999999, MINVAL = -9999999999, SCALE = 0) [ SqlColumnNumber = 4 ];

Property population As %Library.Numeric(MAXVAL = 99999999999, MINVAL = -99999999999, SCALE = 0) [ SqlColumnNumber = 5 ];

Property gdp As %Library.Numeric(MAXVAL = 99999999999999, MINVAL = -99999999999999, SCALE = 0) [ SqlColumnNumber = 6 ];

Property capital As %Library.String(MAXLEN = 60) [ SqlColumnNumber = 7 ];

Property tld As %Library.String(MAXLEN = 5) [ SqlColumnNumber = 8 ];

Property flag As %Library.String(MAXLEN = 255) [ SqlColumnNumber = 9 ];

Parameter USEEXTENTSET = 1;

/// Bitmap Extent Index auto-generated by DDL CREATE TABLE statement.  Do not edit the SqlName of this index.

Index DDLBEIndex [ Extent, SqlName = "%%DDLBEIndex", Type = bitmap ];

/// DDL Primary Key Specification

Index WORLDPKey2 On name [ PrimaryKey, Type = index, Unique ];

}

Это отличная заготовка для разработки своего приложения в объектно-ориентированном стиле. Достаточно добавить методы в класс на ObjectScript, имеющий готовые привязки к базе данных. Фактически, методы этого класса являются хранимыми процедурами, если использовать терминологию SQL-подхода. 

Пробуем реализовать тот же пример, что выше делали на SQL, Добавляем в класс User.world метод WhereName, который играет роль конструктора объекта "информация о стране"по заданному названию страны:

ClassMethod WhereName(name As %String) As User.world

{

    Set id = 1

    While ( ..%ExistsId(id) ) {

        Set countryInfo = ..%OpenId(id)

        if ( countryInfo.name = name ) { Return countryInfo }

        Set id = id + 1

    }

    Return countryInfo = ""

}

Проверяем в терминале:

set countryInfo = ##class(User.world).WhereName("France")

write countryInfo.name

write countryInfo.population

Из этого примерна видно, что для поиска нужного объекта по названию страны, в отличие от SQL запроса,  нам необходимо самостоятельно перебирать поочередно записи в базе данных. В худшем случае, если наш объект в конце списка или его вообще нет, перебирать придётся все записи. Про ускорение поиска через индексированные поля объекта и автогенерацию методов класса в IRIS разговор особый — поищите сами в документации и в публикациях на портале сообщества разработчиков.

Например, для нашего класса, зная сгенерированное IRIS имя индекса по названию страны WORLDPKey2 можно инициализировать/конструировать объект из базы данных одним скоростным запросом:

set countryInfo = ##class(User.world).WORLDPKey2Open("France")

Проверяем так же:

write countryInfo.name

write countryInfo.population

Хорошие рекомендации по выбору между объектным и SQL доступом к хранимым объектам есть в документации. Хотя, в любом случае, вы вольны для своих задач использовать только один из них на 100%.

Плюс ещё и том, что благодаря наличию в IRIS готовых бинарных связок с распространёнными ООП языками, какими как Java, Python, С, C# (.Net), JavaScript и, даже быстро набирающему популярность, Julia [1][2]. Всегда есть возможность выбора удобных вам языковых средств разработки.  

Теперь приступим непосредственно к разговору о данных в веб-API.

REST он же RESTful веб-API

Вырываемся за границы сервера и уютного терминала. Пробираемся поближе к массовым интерфейсам — браузерам и им подобным приложениям. Туда, где в основе взаимодействия систем используются гипертекстовые протоколы семейства http. В IRIS "из коробки" для этого работает связка из собственно сервера баз данных и http-сервера Apache.

Representational state transfer (REST) — архитектурный стиль проектирования распределённых приложений и, в частности, веб-приложений. Ему, между прочим, пошел уже третий десяток лет. REST применяется повсеместно не имея какой-либо стандартизации, кроме как опоры на протоколы http/https. Совсем не то, что его коллеги/конкуренты в веб-службах на базе стандартов для протоколов SOAP и XML-RPC. 

Глобальным ID в REST является URL и он определяет каждую очередную информационную единицу при обмене с базой данных или с бекэнд приложением. 

Документация по разработке REST-сервисов в IRIS.

В нашем примере основой идентификатором будет что-то вроде основы из адреса сервера IRIS http://localhost:52773 и приставленного к нему пути до наших данных /world/ или более конкретно по стране /world/France. 

Примерно так в докер-контейнере:

http://localhost:52773/world/France

При разработке полноценного приложения в документации IRIS рекомендуется воспользоваться одним из трёх генераторов кода. Например, один из них основан на описании REST API по спецификации OpenAPI 2.0. 

Мы пойдём простым путём — реализуем API вручную. В нашем примере сделаем простейшее REST-решение, которое требует всего два шага в IRIS:

  1. Создать класс-диспетчер путей в URL, который будет унаследован от системного класса %CSP.REST

  2. Добавить обращение к нашему классу-диспетчеру при настройке веб-приложения IRIS

Шаг 1 Класс-диспетчер

Реализация класса должна быть достаточно понятна. Делаем по инструкции в документации для "ручного" REST: https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=GREST_csprest#GREST_csprest_urlmap

/// Description
Class User.worldrest Extends %CSP.REST
{
Parameter UseSession As Integer = 1;
Parameter CHARSET = "utf-8";
XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
<Routes>
    <Route Url="/:name" Method="GET" Call="countryInfo" />
</Routes>
}
}

И внутри добавляем метод-обработчик ровно так же, как были устроены вызовы в терминале из предыдущего примера:

ClassMethod countryInfo(name As %String) As %Status
{
    set countryInfo = ##class(User.world).WhereName(name)
    write "Country: ", countryInfo.name
    write "<br>"
    write "Population: ", countryInfo.population
    return $$$OK
}

Как можете видеть, для передачи из входящего REST-запроса в параметра name вызываемого метода-обработчика в диспетчере указывается параметр с двоеточием в начале ":name".

Шаг 2 Настройка веб-приложения IRIS

В меню System Administration > Security > Applications > Web Applications

добавляем новое веб-приложение с указанием точки входа по URL на /world и обработчик — наш класс-диспетчер worldrest. 

Лайфхаки

Веб-приложение после настройки должно сразу отзываться по ссылке http://localhost:52773/world/France (регистр букв для передачи данных запроса в  параметр метода важен, должно быть как в базе данных). 

При необходимости используйте инструменты отладки — хорошее описание есть в этой статье из двух частей (в комментариях загляните тоже). https://community.intersystems.com/post/debugging-web

Если появляется ошибка "401 Unauthorized", а вы уверены, что класс-диспетчер есть на сервере и ссылка правильная, то попробуйте добавить в настройках веб-приложения роль %All во вкладке "Application Roles". Это не совсем безопасно и надо понимать что вы разрешаете, но для локальной работы допустимо.

GraphQL

Эта территория новая, в том смысле, что в документации IRIS об API с использованием GraphQL вы ничего не найдёте сегодня. Но это не помешает нам воспользоваться таким замечательным инструментом.

Всего пять лет как GraphQL появился на публике.  

Это язык запросов для API. И, наверное, это лучшая технология, которая возникла при совершенствовании REST-архитектуры и различных веб-API. Развитием GraphQL занимается Linux фонд. Небольшая вводная статья для начинающих.

А благодаря стараниям энтузиастов и инженеров InterSystems воспользоваться возможностями GraphQL в IRIS можно с 2018 года.

Статья на Хабре: Как я реализовал GraphQL для платформ компании InterSystems

En: GraphQL понимаем, объясняем, внедряем

Приложение для GraphQL состоит из двух модулей — бекенд приложения на стороне IRIS и фронтэнд части, работающей в браузере. То есть, необходимо настроить по инструкции для веб-приложения GraphQL и GraphiQL

Для примера, как это выглядит у меня на IRIS в докер-контейнере. Настройки веб-приложения GraphQL выполняющее функции REST-диспетчера и обработчика схемы базы данных:  

И второе приложение GraphiQL — пользовательских интерфейс для браузера, написан на HTML и JavaScript:

Запускает по адресу http://localhost:52773/graphiql/index.html

Без дополнительных ограничительных настроек, приложение сразу подхватывает все схемы баз данных, доступных в области установки. Это значит, что наши примеры работать начинают сразу. Плюс во фронтэнде есть замечательная организация подсказок из доступных объектов.

Пример GraphQL запроса к нашей базе данных:

{
  User_world ( name: France ) {
    name
    population
  }
} 

И соотвествующий ему ответ:

{
  "data": {
    "User_world": [
      {
        "name": "France",
        "population": 65906000
      }
    ]
  }
}

Так это выглядит в браузере: 

Итоги


Возраст технологии

Пример запроса

SQL

50 лет 

Кодд

SELECT population 
  FROM world 
WHERE name = 'France'

ООП

40 лет 

Кэй, Ингаллс

set countryInfo = ##class(User.world).WhereName("France")

REST

20 лет 

Филдинг

http://localhost:52773/world/France

GraphQL

5 лет 

Байрон

{ User_world ( name: France ) {
name
population 
}

  1. Жизненной силы и энергии у технологий SQL, REST и, теперь наверное, GraphQL много и это надолго. Они все прекрасно уживаются внутри платформы IRIS и, при должном внимании разработчика, дарят радость в создании программ работы с данными.

  2. Хоть это не упомянуто в статье, другие великолепные API на основе XML (SOAP) и JSON в IRIS также реализованы на должном уровне. Пользуйтесь на здоровье.

  3. Помните, что любой обмен данными через API — это всё же неполноценная, а урезанная версия передачи объектов. И задача корректной передачи информации о типа данных в объекте, потерянной в API остаётся, на разработчике, а не в коде.

Вопрос к вам, дорогие читатели

Эта статья готовилась не только для сравнения современных API и, даже, не столько для обзора базовых возможностей IRIS. Больше пользы от примеров выше смогут получить начинающие программисты: увидеть легкость в переключении между API при доступе к базе данных, сделать первые шаги в IRIS, получить быстрый результат для своей задачи.

И поэтому, очень интересно ваше мнение, помогает ли такой подход для "легкого старта", какие шаги процесса доставляют сложность для начинающих осваивать инструменты работы с API в IRIS? Что показалось "неочевидным препятствием"? Поспрашивайте у осваивающих IRIS и напишите мне в комментариях. Думаю, это всем будет полезно обсудить.