image PHP с начала своих времён славен (и критикуем) тем, что поддерживает интеграцию с массой библиотек, а также с практически со всеми СУБД существующими на рынке. Однако в силу каких-то странных причин в нём не было поддержки иерархических баз данных на глобалах.

Глобалы – это структуры для хранения иерархической информации. Они чем-то напоминают базы “key -> value” только с тем отличием, что ключ может быть многоуровневым:

Set ^inn("1234567890", "city") = "Moscow"
Set ^inn("1234567890", "city", "street") = "Req Square"
Set ^inn("1234567890", "city", "street", "house") = 1
Set ^inn("1234567890", "year") = 1970
Set ^inn("1234567890", "name", "first") = "Vladimir"
Set ^inn("1234567890", "name", "last") = "Ivanov"

В этом примере на встроенном языке ObjectScript в глобал ^inn, который хранится на жёстком диске (об этом говорит значок ^ перед названием глобала), сохраняется разноуровневая информация.

Естественно, для работы с глобалами из PHP нам потребуются новые функции, которые добавит PHP-модуль, о котором речь пойдёт ниже.

Глобалы поддерживают множество функций для работы с иерархичностью: обход ключей на каждом уровне отдельно, удаление, копирование и вставка целых деревьев и отдельных узлов. Ну а также как и любая хорошая БД ACID транзакции. Всё это происходит чрезвычайно быстро (порядка 105-106 операций вставки в секунду на обычном железе), по двум причинам:

  1. Глобалы – это более низкоуровневая абстракция, чем SQL,
  2. Базы на глобалах уже в продакшене десятки лет и за это время код их успели вылизать и досконально оптимизировать.

Подробнее о глобалах в цикле статей “Глобалы — мечи-кладенцы для хранения данных”:

Деревья. Часть 1.
Деревья. Часть 2.
Разреженные массивы. Часть 3.

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

Я люблю PHP (и разрабатываю на нём) и хотел поиграться с глобалами. Готового модуля не было. Я написал в InterSystems c просьбой создать его. Ожидание ни к чему не привело, и в итоге мы (я вместе со своим студентом-дипломником) сделали модуль сами. InterSystems проспонсировала разработку в рамках образовательного гранта.

Вообще говоря, InterSystems IRIS – это мультимодельная СУБД, поэтому из PHP с ней можно работать через ODBC, используя SQL, но меня интересовали именно глобалы, а такого коннектора не было.

Итак, модуль доступен для PHP 7.x (тестировался под 7.0-7.2). На текущий момент он может работать только с InterSystems IRIS и Caché, установленной на том же хосте.

Страница модуля на OpenExchange (каталог проектов и дополнений для разработчиков на InterSystems IRIS и Caché).

Там есть полезный раздел DISCUSS, в котором люди делятся опытом использования.

Скачать тут:
https://github.com/intersystems-community/php_ext_iris
Скачать репозиторий из командной строки:

git clone https://github.com/intersystems-community/php_ext_iris

Инструкция по установке модуля на английском и русском языках.

Функции модуля:

PHP-функция Описание
Работа с данными
iris_set($node, value)

Установка узла.

  1. iris_set($global, $subscript1, …, $subscriptN, $value);
    iris_set($global, $value);

    Возвращает: true или false (при ошибке).
    Все параметры функции являются строками или числами. Первый – имя глобала, далее индексы, последний параметр – значение.

    iris_set('^time',1);
    iris_set('^time', 'tree', 1, 1, 'value');

    Аналог на ObjectScript

    Set ^time = 1
    Set ^time("tree", 1, 1) = "value"
  2. iris_set($arrayGlobal, $value); Всего 2 параметра: первый – массив, в котором хранятся имя глобала и все его индексы; второй – значение.
    $node = ['^time', 'tree', 1, 1];
    iris_set($node,'value');
iris_get($node)

Чтение узла.

Возвращает: значение (число или строка), NULL (значение не определено) или FALSE (при ошибке).

  1. iris_get($global, $subscript1, …, $subscriptN);
    iris_get($global);
    Все параметры функции являются строками или числами. Первый – имя глобала, далее индексы. Глобал может не иметь индексов.

    $res = iris_get('^time');
    $res1 = iris_get('^time', 'tree', 1, 1);
  2. iris_get($arrayGlobal); Единственный параметр – массив, в котором хранятся имя глобала и все его индексы.
    $node = ['^time', 'tree', 1, 1];
    $res = iris_get($node);
iris_zkill($node)

Удаление значения узла.

Возвращает: TRUE или FALSE – при ошибке.

Важно отметить, что эта функция удаляет только значение в узле и не трогает нижележащие ветви.

  1. iris_zkill($global, $subscript1, …, $subscriptN);
    iris_zkill($global);
    Все параметры функции являются строками или числами. Первый – имя глобала, далее индексы. Глобал может не иметь индексов.

    $res = iris_zkill('^time'); // Нижележащие ветви не удаляются.
    $res1 = iris_zkill('^time', 'tree', 1, 1);
  2. iris_zkill($arrayGlobal);Единственный параметр – массив, в котором хранятся имя глобала и все его индексы.
    $a = ['^time', 'tree', 1, 1];
    $res = iris_zkill($a);
iris_kill($node)

Удаление узла и всех ветвей-потомков.

Возвращает: TRUE или FALSE – при ошибке.

  1. iris_kill($global, $subscript1, …, $subscriptN);
    iris_kill($global);
    Все параметры функции являются строками или числами. Первый – имя глобала, далее индексы. Глобал может не иметь индексов, в этом случае он удаляется полностью.

    $res1 = iris_kill('^example', 'subscript1', 'subscript2');
    $res = iris_kill('^time'); // Удаляется глобал целиком.
    
  2. iris_kill($arrayGlobal);Единственный параметр – массив, в котором хранятся имя глобала и все его индексы.
    $a = ['^time', 'tree', 1, 1];
    $res = iris_kill($a);
iris_order($node)

Обход ветвей глобала на заданном уровне

Возвращает: массив, в котором содержится полное имя следующего узла глобала или FALSE (при ошибке).

  1. iris_order($global, $subscript1, …, $subscriptN); Все параметры функции являются строками или числами. Первый – имя глобала, далее индексы.

    Форма употребления в PHP и аналог на ObjectScript:

    iris_order('^ccc','new2','res2'); // $Order(^ccc("new2", "res2"))
  2. iris_order($arrayGlobal); Единственный параметр – массив, в котором хранятся имя глобала и индексы начального узла.
    $node = ['^inn', '1234567890', 'city'];
    for (; $node !== NULL; $node = iris_order($node))
    {
      echo join(', ', $node).'='.iris_get($node)."\n";
    }

    Даст нам вывод:

    ^inn, 1234567890, city=Moscow
    ^inn, 1234567890, year=1970
iris_order_rev($node)

Обход ветвей глобала на заданном уровне в обратном порядке

Возвращает: массив, в котором содержится полное имя предыдущего узла глобала на этом же уровне или FALSE (при ошибке).

  1. iris_order_rev($global, $subscript1, …, $subscriptN);
    Все параметры функции являются строками или числами. Первый – имя глобала, далее индексы.

    Форма употребления в PHP и аналог на ObjectScript:

    iris_order_rev('^ccc','new2','res2'); // $Order(^ccc("new2", "res2"), -1)
  2. iris_order_rev($arrayGlobal);
    Единственный параметр – массив, в котором хранятся имя глобала и индексы начального узла.

    $node = ['^inn', '1234567890', 'name', 'last'];
    for (; $node !== NULL; $node = iris_order_rev($node))
    {
      echo join(', ', $node).'='.iris_get($node)."\n";
    }

    Даст нам вывод:

    ^inn, 1234567890, name, last=Ivanov
    ^inn, 1234567890, name, first=Vladimir
iris_query($CmdLine)

Обход ветвей глобала c заходом в низлежащие уровни

Возвращает: массив, в котором содержится полное имя нижележащего узла (при наличии) или следующего узла глобала (при отсутствии вложенного узла).

  1. iris_query($global, $subscript1, …, $subscriptN); Все параметры функции являются строками или числами. Первый – имя глобала, далее индексы.

    Форма употребления в PHP и аналог на ObjectScript:

    iris_query('^ccc', 'new2', 'res2'); // $Query(^ccc("new2", "res2"))
  2. iris_query($arrayGlobal); Единственный параметр – массив, в котором хранятся имя глобала и индексы начального узла.
    $node = ['^inn', 'city'];
    for (; $node !== NULL; $node = iris_query($node))
    {
      echo join(', ', $node).'='.iris_get($node)."\n";
    }

    Даст нам вывод:

    ^inn, 1234567890, city=Moscow
    ^inn, 1234567890, city, street=Req Square
    ^inn, 1234567890, city, street, house=1
    ^inn, 1234567890, name, first=Vladimir
    ^inn, 1234567890, name, last=Ivanov
    ^inn, 1234567890, year=1970

Порядок отличается от порядка в котором мы устанавливали, так как в глобале автоматически при вставке всё сортируется по возрастанию.

Сервисные функции
iris_set_dir($FullPath)

Установка директории с БД

Возвращает: TRUE или FALSE – при ошибке.

iris_set_dir('/InterSystems/Cache/mgr');

Нужно выполнить перед соединением с базой.

iris_exec($CmdLine)

Выполнить команду БД

Возвращает: TRUE или FALSE – при ошибке.

iris_exec('kill ^global(6)'); // Команда на ObjectScript для удаления глобала
iris_connect($login, $pass) Подключиться к БД
iris_quit() Разорвать соединение с БД
iris_errno() Получить код ошибки
iris_error() Получить текстовое описание ошибки

Если вы хотите самостоятельно поиграться с модулем, то:

Специально для пользователей Хабра был создан Dockerfile для сборки образа.

git clone https://github.com/intersystems-community/php_ext_iris
cd php_ext_iris/iris
docker-compose build
docker-compose up -d

Тестируем демо-страницу на localhost:52080 в браузере.

PHP файлы, которые можно править и играться с ними лежат в папке php/demo и будут подмонтированы внутрь контейнера.

Чтобы потестировать IRIS используйте логин admin, пароль SYS.

Чтобы войти в настройки IRIS используйте следующий УРЛ:
http://localhost:52773/csp/sys/UtilHome.csp

Чтобы войти в консоль IRIS этого контейнера используем команду:

docker exec -it iris_iris_1 iris session IRIS

Специально для пользователей Хабра и InterSystems Caché была поднята виртуалка с php-модулем.

Демо-страница на русском языке.
Demo page on english.
Login: habr_test
Password: burmur#@8765

Для самостоятельной установки модуля под InterSystems Caché

  1. Иметь Linux. Я тестировал под Ubuntu, под Windows модуль тоже должен собираться, но я это не тестировал.
  2. Скачать бесплатную версию:
  3. Установить модуль cach.so в PHP по инструкции.

В docker-контейнере на своём персональном компьютере (AMD FX-9370@4700Mhz 32GB, LVM, SATA SSD) для интереса я сделал два примитивных теста на скорость вставки новых значений в БД.

  • Вставка 1 миллиона новых элементов в глобал заняла 1.81 секунды или 552К инсертов в секунду.
  • Обновление значения в одном и том же глобале 1000000 раз заняла 1.98 секунды или 505K обновлений в секунду. Интересный факт, что вставка происходит, чем обновление. Видимо это следствие изначальной оптимизации базы данных под быструю вставку.

Понятно, что эти тесты не могут претендовать на точность и полезность, так как примитивны, сделаны в контейнере. На более мощном железе с дисковой системой на PCIe SSD можно добиться десятков миллионов вставок в секунду.

Что можно доделать и текущее состояние

  1. Можно добавить полезные функции для работы с транзакциями (их и сейчас можно использовать через iris_exec).
  2. Не реализована функция возврата всей структуры глобала, чтобы из PHP не заниматься обходом глобала.
  3. Не реализована функция сохранения PHP-массива как поддерева.
  4. Не реализован доступ к локальным переменным базы. Только через iris_exec, хотя лучше через iris_set.
  5. Не реализован обход глобала в глубь в обратном направлении.
  6. Не реализован доступ к БД через объект с использованием методов (аналогичным текущим функциям).

Для продакшена текущий модуль, пожалуй, ещё не готов: не было тестов под высокие нагрузки и на утечки памяти. Однако если это кому-то потребуется, то обращайтесь ко мне (Сергей Каменев updates@mail.ru).

Заключение

Долгое время миры PHP и иерархических баз на глобалах практически не пересекались, хотя глобалы предоставляют сильный и быстрый функционал на специфических типах данных (медицинские, персональные).

Надеюсь, что этот модуль послужит толчком для экспериментов программистов PHP с глобалами и программистам ObjectScript для простой разработки веб-интерфейсов на PHP.

P.S. Спасибо за внимание!

Оригинал статьи

Оставьте комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *