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

Структура

  • Начальная проблема и причины начала работы

  • Какие есть варианты попроще и чем они не нравятся

  • Что и как сделано

  • Результат

Начальная проблема и причины начала работы

О школе у меня осталось много воспоминаний, со временем они все стали хорошими, одну историю я часто вспоминаю с улыбкой:

Средние классы школы, учительница по математике пытается проанализировать контрольные работы и объяснить свое нежелание ставить пятерку отличницам, которые превратили решение математической задачи в сочинение на тему, как эта задача решалась. Поняв, что объяснения до детей не доходят, она взяла мою тетрадь и продемонстрировала со словами, которые я решил считать похвалой: «Я очень люблю его работы проверять. Это настолько ленивый человек, что любая лишняя или нематематическая запись у него никогда не будет написана. Всегда использует сокращения и ленится писать слова, если есть совпадающие с ними по смыслу символы». Кстати, несмотря на похвалу, я тоже пятерку тогда не получил.

Думаю, что здоровая лень – это положительное качество. Код, в котором его автор поленился писать однотипные вызовы и оформил его один раз в процедуре, давно стал признаком качества написания. Ну а функционал различных систем, который на 90% делает одно и то же, у меня вызывает вопрос – «не лень же было столько копипастить?».

Хочу на примере показать, что иногда асинхронно проще проходит реализация снаружи, чем внутри систем. Два разработчика базы данных (БД) MSSQL и Oracle в процессе разработки сложного бизнес-процесса столкнулись с необходимостью обмена файлами между своими системами. На этапе разработки у них отлично пошел обмен в расшаренной папке, которая смонтирована к обоим серверам БД и доступна для обращения.  При тестировании увеличилась нагрузка на обмен файлами и появилась на регулярной основе EXCEPTION о занятом другим модулем файле. Поскольку менять весь бизнес-процесс уже накладно по трудозатратам, то обмен стал обрастать условиями и сложностями – разделили направления обмена на несколько каталогов. Добавили транзакционность в обмен – формировали пустые файлы .lock для монопольного доступа к каталогам и проверяли их наличие. Когда заработало все более или менее стабильно, по их работе прошелся бич безопасности, выявив дыры в этой самой безопасности, и им быстренько (обычно в такой момент время не ждет – все, как у Джека Лондона) написали в модной микросервисной архитектуре службу обмена файлами. При рассмотрении поближе этот микросервис оказался по сути тем, что раньше называлось толстым клиентом, – в одной сессии открывается SELECT с достаточно жесткой WHERE-частью и забирается бинарник файла, в другой сессии происходит INSERT забранных бинарников в таблицу в цикле. Все заработало, все службы довольны.

Потом приносящий прибыль бизнес-процесс продублировали, и не один раз. И в конце примера пришла пора исправлений существующих микросервисов: то ли поле добавлено в INSERT, то ли WHERE-часть усложнилась в SELECT – в общем, надо менять код программ. И делать это надо ответственному за эти функционалы программисту. Тут здоровая лень и просыпается – кажется бессмысленным компилить однотипные изменения в n-проектах, если n>1. А ведь есть еще понимание, что поддержка таких изменений будет производиться на достаточно регулярной основе. Появилось желание пересмотреть подход к организации подобных обменов. Как я это делал, напишу ниже.

Какие есть варианты попроще и чем они не нравятся

Сейчас есть такой модный тренд – роботизация. Я столкнулся с использованием робота в автоматизировании передачи данных из одной системы в другую. Выглядит это так: поднимается виртуалка с установленными на ней необходимыми программами, в данном случае клиентская часть модуля и Excel, робот по сути – это кликер на определенные части экрана, где есть кнопки управления в клиенте или в Excel (имитирует нажатие этих кнопок человеком). В виртуалке возникли сложности с имитацией среды выполнения макросов в шаблоне Excel, заработало на 90% без некоторых отработок, возникла необходимость повесить еще службу для этих отработок, чтобы полноценно имитировать функционал. Это решение имеет неприятные минусы. Для настройки одного макроса кликера нужно провести довольно кропотливую и трудоемкую работу (для настройки виртуалки под требуемую задачу). Обязательно всплывают нюансы, на которые уйдет время и, возможно, ресурсы разработки. Полная зависимость от версий используемых программ, изменения интерфейса рушат весь механизм имитации. Разработчик макросов должен иметь компетенции в используемых модулях программ. Это постоянная работа (не для ленивого человека) по исправлению макросов, количество которых будет расти. На память из юности приходят кликеры игрушек ММОРПГ – производитель такого кликера всегда хочет получать деньги за каждую сессию кликера, – в нашем случае зарплату оператора системы. Сложно и недостаточно надежно, придется сверху отдельный мониторинг еще прикрутить по каждому процессу. Думаю, этот подход применим к старым системам, неизменяемым годами. Новые лучше обслуживать более простыми способами.

И еще один момент, который не нравится в различных вариантах автоматизации процессов внутри системы, – ограничение на работу только доступными данными внутри системы. Для того чтобы организовать выборку данных по внешним ключам, обязательно нужно дорабатывать АПИ обмена какой-нибудь, или список фильтров организовывать, или еще какой сложный процесс, который не покроет следующую выборку и его надо будет опять дорабатывать в коде.

Что и как сделано

Для себя я выделил такие особенности, которые хочу видеть в системе автоматизации:

  • должен быть набор инструментов для выполнения требуемых операций;

  • должен быть механизм настройки, понятный и сразу воспринимаемый другими разработчиками;

Чтобы создать набор инструментов, я взял все простые операции, которые выполнялись при функционировании автоматизируемых бизнес-процессов, и сгруппировал их по используемым библиотекам. У меня получились на тот момент такие группы:

  • Работа с файлами (удаление, копирование, перенос, перевод в BASE64 и т. п.);

  • Чтение/запись данных из Excel, включая CSV и т. п. Здесь в группу попали все операции, которые можно выполнить библиотекой NPOI (Apache POI);

  • Работа с JPG и PDF, тут пришлось сделать специфическую DLL под индивидуальные запросы, пример: на все графические файлы поставить печать и объединить в PDF;

  • Выполнение анонимного блока для Oracle;

  • Выполнение анонимного блока для MSSQL;

  • Использование библиотеки WinSCP (SFTP, FTP, WebDAV, S3 and SCP client);

  • Использование библиотеки RabbitMQ, у нас эта очередь популярна;

  • Работа с почтовыми серверами;

  • Использование CRYPTO PRO.

У меня напрашивались отдельные модули с классами, имеющими общий интерфейс для запуска обработки, который принимает список входных параметров (имя, значение) из нескольких строк и отдает результат работы в виде такого же списка выходных параметров:

public interface IEngineTask
{
        Dictionary<string, Dictionary<string, string>> RunTask(Dictionary<string, Dictionary<string, string>> ValueInput, Logger InLogger);
}

Ну и интерфейс логирования. Как видите, в него тоже добавил при реализации.

Необходимо быстрое наращивание функциональных возможностей. Тут самым простым видится поиск плагинов в каталоге службы при запуске. Прошли по DLL, нашли те, которые поддерживают интерфейс обмена, зарегистрировали для использования, несколько строк по сути при старте службы:

foreach (string aff in Directory.GetFiles(HomeDllDirectory, "*.dll", SearchOption.TopDirectoryOnly))
    foreach (Type rowType in Assembly.LoadFile(aff).GetTypes())
        if (rowType.GetInterface("IEngineTask") != null)
          ....

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

Хранение настроек проблем не вызвало. Таблица процессов (дальше я их буду называть конвейерами), таблица асинхронных вызовов того или иного класса (их буду называть тасками) и таблица связи между тасками одного конвейера. Также нужна таблица для хранения текущих значений входных и выходных параметров тасков и статус их выполнения.

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

Простой граф

Посложнее, с долгой обработкой, на рисунке видны уже пройденные узлы (зеленые), текущее выполнение(желтые) и очередь на выполнение (серые)

Теперь мы подошли к той части, которая определит, как часто необходимо вмешательство разработчика для внесения изменений в логику работы. Напомню, хочется пойти на поводу у лени и не вмешиваться вообще, отдав в эксплуатацию инструмент, и развивать этот инструмент, не вдаваясь в проблемы «индейцев». Это можно сделать, если формирование входных данных – известный всем механизм. Поскольку используемая БД – Oracle, то механизм формирования входных данных – это запрос на PL/SQL.

Полностью на PL/SQL будут запросы для сбора данных, которым не нужны результаты работы предыдущих тасков. Например, поиск файлов в каталоге 'C:\dir_name\':

SELECT 'c:\dir_name\' AS SourceName
      ,'*.*' AS MaskFile
      ,'0' AS EngineType
      ,'Поиск файла в каталоге SourceName по маске MaskFile' AS EngineName 
  FROM DUAL

Когда возникает потребность в результатах работы предыдущих тасков, их надо как-то обозначить в запросе, я ввел одно макроопределение #XMLTABLE, оно при анализе запроса заменяется на подзапрос из всех результатов работы тасков (стрелочка с направлением передачи параметров на рисунках), я подробнее об этом напишу ниже. Вот как выглядят два запроса для последовательно идущих тасков, их результат – поиск в папке SFTP-файлов и перенос их в локальную шару. Поля в запросе – это параметры соединения SFTP и пути каталогов, таск их умеет обрабатывать.

Получение списка файлов:

SELECT '/sftp/in' AS SourceName, 'HostName' AS SHostName, 'Port' AS SPort
      ,'UserName' AS SUserName, 'Password' AS SPassword     
      ,'SshHostKeyFingerprint' AS SHostKeyFingerprint
      ,'SshPrivateKeyPath' AS SPrivateKeyPath     
      ,'0' AS EngineType, 'Поиск файлов в SourceName SshFTP' AS EngineName
  FROM DUAL

Перенос файлов в каталог 'C:\dir_name\', при условии код ошибки = 0 и файл не содержит расширение 'filepart', т. е. он закачан полностью:

WITH i AS (#XMLTABLE)
SELECT i.SOURCENAME AS SourceName, 'HostName' AS SHostName, 'Port' AS SPort
      ,'UserName' AS SUserName, 'Password' AS SPassword     
      ,'SshHostKeyFingerprint' AS SHostKeyFingerprint
      ,'SshPrivateKeyPath' AS SPrivateKeyPath     
      ,'C:\dir_name\'||regexp_replace(i.SourceName, '.*[\\/]','') AS DestName
      ,'2' AS EngineType
      ,'Перенос\копирование файла SourceName с SshFTP в каталог DestName' AS EngineName
      ,'1' AS SRemove,'1' AS SOverWrite
      ,i.ERRORCODE, i.ERRORMESSAGE 
  FROM i 
 WHERE i.ERRORCODE=0 AND CASE WHEN SUBSTR(LOWER(i.SOURCENAME),-8)='filepart' THEN 1 ELSE 0 END=0

Как видите, запросы прозрачны, просты и позволяют в них быстро ориентироваться при внесении изменений.

Общая последовательность формирования входных данных такая:

  • Проверяются все узлы графа конвейера, и находятся те, что еще не отработали, но уже имеют отработанные таски по всем связям на входе таска;

  • Если есть макроопределение #XMLTABLE, то начинается работа генератора подзапроса;

  • Сформированный запрос готов к запуску. Каждый таск имеет необязательный параметр 'ConnectionString'. Если он не заполнен, то запуск осуществляется в текущей служебной сессии, иначе в сессии по значению параметра;

  • Открыть курсор из строки и сериализовать его в строку XML в Oracle можно несколькими строчками, сделано так для того, чтобы пустые поля все равно присутствовали в XML:

PROCEDURE ...
  IS
  v_sql CLOB; -- Строка с запросом
  v_clob CLOB; -- Результат запроса
  i_cursor SYS_REFCURSOR;
  v_h dbms_xmlgen.ctxHandle;
BEGIN
....
  OPEN i_cursor FOR v_sql;
  v_h := dbms_xmlgen.newContext(i_cursor);
  dbms_xmlgen.setNullHandling(v_h, 2);
  v_clob := dbms_xmlgen.getXML(v_h);
  DBMS_XMLGEN.CLOSECONTEXT(v_h);
  • Перед вызовом таска строка с XML десериализуется в объект

Dictionary<string, Dictionary<string, string>>

и передается в таск.

Еще несколько слов о генерации строки с #XMLTABLE. Я добавил к каждому таску признак динамического формирования количества входных параметров. Если он не заполнен, то из всего набора данных в XML интересуют при генерации подзапроса только поля, которые зарегистрированы входными параметрами у таска (если помните, все DLL регистрируются при каждом запуске службы, в параметры регистрации входят уникальный ID, строка соединения с БД,  если нужно это соединение, набор описаний входных и выходных параметров). Если признак заполнен, то приходится опросить всю XML, вытащить все используемые поля и включить их во входные параметры – для больших XML это не быстро.

Результат

После всех изысканий и разработки мы получили такую систему автоматизации бизнес-процессов:

  • Нет ограничений по функционалу, недостающие операции добавляются в существующий плагин, или пишется новый плагин с новой группой операций – простая и нетрудоемкая работа, которую можно поручить даже студенту;

  • Простота поддержки – проблемные места выявляются в отдельном черном ящике и быстро исправляются;

  • Простота и однотипность создания новых бизнес-процессов, без требований к новым областям знаний: создание графа и написание к каждому узлу SELECT с необходимыми входными параметрами;

  • Сняты ограничения по доступности данных на разных узлах системы: например, мы можем сделать выборку из таблицы MSSQL на основе справочника из БД Oracle и имен файлов в каталоге SFTP;

  • Отдельный результат – написание этой статьи. :)

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

P. S. Хочу закончить статью шуткой, не моей, а разработчиков Oracle, с которой столкнулся при тестовых парсингах XML и JSON:

Два запроса простейших, первый «летает», второй «ложится»; судя по форумам, это полечили в последнем патче Oracle. Но подозреваю, что столько нам открытий чудных готовят соединения в запросах XMLTABLE и JSON_TABLE:

  • Быстрый:

SELECT jt.*
  FROM DUAL,
    JSON_TABLE
      ('{"name":"test","content":"MTIzCg=="}', '$'
        COLUMNS 
          (file$name VARCHAR2(255) PATH '$.name'
          ,content CLOB PATH '$.content'
          )
      ) jt
  • Медленный:

SELECT jt.*
  FROM JSON_TABLE
      ('{"name":"test","content":"MTIzCg=="}', '$'
        COLUMNS 
          (file$name VARCHAR2(255) PATH '$.name'
          ,content CLOB PATH '$.content'
          )
      ) jt

Комментарии (5)


  1. caballero
    30.08.2021 20:19

    Типичный пост начинающего програмиста. Смотрите какич ч написал класы с sql скрипты и какие задействовал модные технологии.

    Непонятно только одно каким боком все это к ERP.


    1. innerjoin Автор
      30.08.2021 20:45

      Из за деревьев не видно леса. Для понимания смысла вложенного в статью - максимальная универсальность в создании модулей взаимодействия между различными процессами, правильно подойти к этому вопросу - и на выходе инструмент, позволяющий штамповать процессы деятельности предприятия в real-time.


  1. xtender
    02.09.2021 01:47

    Два запроса простейших, первый «летает», второй «ложится»; судя по форумам, это полечили в последнем патче Oracle.

    На какой версии это было? Явно >12.1.0.2 и <18.3


    1. innerjoin Автор
      04.09.2021 11:48

      На 12.1.0.2 потребовалось привлекать админов DBA, иначе Oracle выдавал ошибку на CLOB`е - ORA-40484: недопустимый тип данных для столбца JSON_TABLE, но оптимизация запроса все равно не работала и запросы вели себя как описал выше.

      У нас уже пропатчили до Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 и проблема медленного запроса исчезла.


      1. innerjoin Автор
        04.09.2021 12:52

        Если интересно по каким граблям я прошел по парсингам, вот пример из свежей версии Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production, он до сих пор не починен у оракла, вопрос задал им о причине, но пока не ответили:

        Делаем "чистый" SQL, чтоб не трогать интерпретатор PL кода:

        WITH i AS (SELECT xt.* FROM xmltable ('/ROWSET/*' passing xmltype(TO_CLOB('<ROWSET>
          <ROW><TESTFIELD>{"name":"test1","content":"test1"}</TESTFIELD></ROW><ROW><TESTFIELD>{"name":"test2","content":"test2"}</TESTFIELD></ROW><ROW><TESTFIELD>{"name":"test3","content":"test3"}</TESTFIELD></ROW>
        </ROWSET>')) COLUMNS testfield CLOB PATH 'TESTFIELD') xt
        )
        SELECT * FROM i, JSON_TABLE(i.testfield, '$' COLUMNS (tname VARCHAR2(255) PATH '$.name', tcontent CLOB PATH '$.content')) jt

        Этот запрос дает пустой курсор на выходе, хотя данные в нем есть ка видите.

        Добавляем в запрос одно PL-ное ROWNUM AS r$n поле перед xt.*, у нас отрабатывает интерпретатор кода PL/SQL и вот обида - у нас заполнен курсор з-мя строками.

        WITH i AS (SELECT ROWNUM AS r$n, xt.* FROM xmltable ('/ROWSET/*' passing xmltype(TO_CLOB('<ROWSET>
          <ROW><TESTFIELD>{"name":"test1","content":"test1"}</TESTFIELD></ROW><ROW><TESTFIELD>{"name":"test2","content":"test2"}</TESTFIELD></ROW><ROW><TESTFIELD>{"name":"test3","content":"test3"}</TESTFIELD></ROW>
        </ROWSET>')) COLUMNS testfield CLOB PATH 'TESTFIELD') xt
        )
        SELECT * FROM i, JSON_TABLE(i.testfield, '$' COLUMNS (tname VARCHAR2(255) PATH '$.name', tcontent CLOB PATH '$.content')) jt

        Ну как так, ну не должен один и тот же запрос на 2-х родных интерпретаторах языка давать разный результат от добавления или удаления полей.

        Не поймите меня как Oracle-ненавистника, с версии 9i я считаю эту БД лучшей в мире (и здесь не кидайте камни, это мое личное мнение), когда они в 12-й версии сделали реальную полноценную рекурсию в запросе, захотелось снять шляпу, да же так - снимаю шляпу за такой запрос:

        WITH t(y,k) AS ( SELECT 1 AS l, 2 AS k FROM DUAL UNION ALL SELECT y+1, k*2 FROM t WHERE y<= 30 ) SELECT * FROM t

        Но тут они явно спешат c JSON_TABLE и это - как солнце облаком на чистом небосклоне закрыло :)