PHP-модуль для работы с иерархическими данными в InterSystems IRIS

    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-модулем.

    Для самостоятельной установки модуля под 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. Спасибо за внимание!
    InterSystems
    InterSystems IRIS: СУБД, ESB, BI, Healthcare

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

    Комментарии 1

      0

      А почему именно iris_kill? Не delete, не remove?
      А, понял.

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

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