Как мы делали Low-Code конструктор для Back Office. Часть 2 (Back-End и база данных)

от автора

Привет, это вторая статья из цикла про наш путь создания Low-Code платформы-конструктора для разработки сложных Back Office систем. В прошлой статье я сформулировал, что такое «сложные системы», задачу, которую необходимо решить, а также привел набор «наблюдений» о принципах построения IT-производства на базе Low-Code инструмента. В этот раз я опишу подход, который мы выбрали для построения Back-End и работы с базой данных. В следующих статьях про принципы организации тонкого клиента.

В институте, как у всех IT-специальностей, у нас был курс про базы данных. Могу честно признаться, что я его ненавидел. Он был не только скучный, но и с какими-то унылыми и пустыми примерами про клиентов и заказы, очевидными связями и формами для заполнения данных. Тогда, в моем кругу, базы данных всегда были вторичны, а вот алгоритмы и языки программирования считались интересными и сложными, и, как следствие, крутыми. Даже если посмотреть на олимпиады по программированию, то там, в целом, одна алгоритмика. После того, как я лет 15 поварился в финтех, могу с уверенностью сказать, что про базы данных не с того начинают и вообще не то читают. Лекторы старательно прячут от вас истинный архитектурный кайф от проектирования, и, вместо этого, вытаскивают наружу неприглядное внутреннее устройство и всякую специфику. После этого кажется, что «таблицы» делать ничего не стоит, а все остальное — это скучнейшие отчеты, мерзенький тюнинг sql запросов и разные проблемы администрирования – крест для админов, которые сами виноваты в выборе своей судьбы. В тоже время, дзен проектирования находится рядом, если осознать, что именно база определяет основные бизнес-объекты продукта и главный функционал через взаимоотношения между ними, а также задает направление UI по ролям пользователей, кто что видит и как ищет. Здесь нужно применять абстрактное мышление, уметь обобщать и выделять главное, предсказывать в какую сторону продукт будет развиваться и проводить экстрасенсорный анализ требований, поскольку они часто «не очень». При этом ты не пишешь ни строчки кода, а просто крупными мазками рисуешь каркас продукта и продумываешь функционал. Ну ведь здорово же!

Базы данных не так часто меняются, как JavaScript-движки, но все равно животный страх привязаться к одному производителю многим не дает спать спокойно. Когда я участвовал в разработке ядра большой финтех системы под Oracle, у нас был негласный указ не только избегать различных подсказок, но и не использовать многотабличные запросы при описании бизнес-логики на PL/SQL. Вместо join-ов были вложенные курсоры (для уверенности, как пойдет запрос), а еще все боролись за возможность автоматической конвертации пакетов Oracle во что-нибудь другое. На текущий момент, в связи с развитием нереляционных баз данных (ну запустятся же они когда-нибудь с полноценным транзакционным режимом), точно хочется сохранить похожий подход при взаимодействии с базой и быть максимально гибким в выборе системы для хранения. Всю внутреннюю бизнес-логику (что может быть написано на PL/pgSQL, PL/SQL и прочих), удобно выносить наружу, чтобы иметь возможность в будущем заниматься линейным масштабированием сервера приложений, а для базы оставить только хранение и поиск данных.

Структура базы данных

Обновление большой базы данных на новую версию – всегда больное местечко. Во-первых, все стоит и ждет. Во-вторых, оно не всегда проходит гладко. Для финтеха это критично, и мы приняли решение организовать хранение данных в «запакованном» виде — почти Schema-less структура, только на честной базе данных PostgreSQL, которую мы поддержали одну из первых. Для индексных полей создаются дополнительные таблицы, но все основные таблицы практически состоят из одного запакованного поля. Для шевеления структуры и upgrade-ов, так на порядок легче, а еще это на шаг ближе к NoSQL, что тоже немаловажно.

Структура базы данных у нас описывается в json файлах, каждая таблица в своем файле. Таблицы делятся на

  • «обычные» — автоматически создаются и upgrade-тся в нашей базе

  • «абстрактные» — структура таблицы описывается как для «обычной» таблицы, но при этом на Java / Kotlin создаются кастомные процедуры для заполнения ее данными из любых внешних (и внутренних) источников. Также можно определить кастомные процедуры создания, обновления и удаления записей, и абстрактная таблица ничем уже не будет отличаться от обычных. Любые view, это тоже «абстрактные» таблицы, для которых написан Java-код

Важно, что при описании логики взаимодействия для разработчика уже нет разницы с какими именно таблицами он работает. Более того, конструктору веб-экранов тоже все равно из таблиц какого типа создавать экраны. Данный подход позволяет конструировать практически любые дашборды на любых источниках данных и использовать Low-Code платформу как инструмент для создания «единых точек входа» или сложных рабочих мест.

Рассмотрим пример из двух таблиц Client и Loan (выданные клиенту ссуды).

Выданные клиенту ссуды

Выданные клиенту ссуды

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

Описание таблиц Client (client.,json) и Loan (loan.json)

Описание таблиц Client (client.,json) и Loan (loan.json)

Замечу, что поле client.parent ссылается на туже самую таблицу для организации иерархии. Поле loan.status — это «короткий справочник», по которому генерируются константы в Java / Kotlin для удобного описания логики поведения. Элементы «@» — технические, создаются автоматически и служат для автоматического обновления структуры базы данных, чтобы не было необходимости вручную создавать alter скрипты и можно было бы легко добавлять, удалять и модифицировать все элементы. В данной структуре кроме типов полей описывается информация о доменах полей, к примеру, на что именно ссылается поле («ref» на какую таблицу), как поступать с текущей строчкой при удалении записи на которую ссылается текущая (элемент «refDelete»), какое поле «отвечает» за название текущей строчки при отображении в справочнике, построенном по данной таблице и прочее. Все это используется, в том числе, во время генерации java-объектов, которые нужны для API при работе с базой данных.

Описание индексов происходит в том же файле, где описывается таблица.

Индекс в таблице Loan

Индекс в таблице Loan

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

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

Процедуры и описание бизнес-логики

Прямого доступа из кода к базе нет, и все select/update/delete операции закрываются специальным API. Это, во-первых, позволяет максимально изолировать бизнес-логику от способа хранения данных, во-вторых, позволяет создавать единообразную работу как с обычными, так и абстрактными таблицами и в-третьих, сильно упрощает «читабельность» кода.

Для примера напишем функцию, которая закрывает все ссуды у всех клиентов, ссылающихся на текущего клиента и так вниз по «дереву» клиентов.

Функция closeAllClientLoans

Функция closeAllClientLoans

Первый метод iterateсоздает курсор и проходит последовательно по всем строчкам указанной таблицы, применяя фильтр parent = ${clientID}, где вместо ${clientID} впечатывается значение входной переменной функции.  Поскольку таблица Client абстрактная, то для нее внутри будет вызван соответствующий класс, где описано, как заполнять ее данными. Для более строгой записи фильтра parent = ${clientID}, чтобы не впечатывать названия полей в строчку, можно заменить на QueryHelper.c().and(Client.fParent, clientID).toString()

Поскольку перед функцией стоит @ActionName("closeAllClientLoans"), то по данной функции будет автоматически сформировано REST API, и функцию можно будет привязать к любой кнопке в экране. Так же через REST можно вызывать самостоятельно все стандартные функции (в том числе все select/update/delete) и строить альтернативных тонких клиентов или экраны «первого» типа (из моей предыдущей статьи), для «людей». Конечно, сверху еще накладывается проверка прав пользователей и различный аудит при необходимости.

Создание новых объектов (строчек в базе данных) происходит похожим образом, к примеру функция createClientLoan создает новую ссуду для клиента.

Создание ссуды у клиента

Создание ссуды у клиента

Важно отметить, что именно таким API к базе данных и его «одинаковостью» к внешним источникам (абстрактным таблицам) мы боролись за «души» так называемых бизнес-разработчиков: за простоту и прозрачность описания логики приложения, отделение логики от технического уровня взаимодействия с базой и тонким клиентом, снятие головных болей со сборками и обновлениями структуры данных. Более того, если функция, вызываемая снаружи (через API) выбрасывает exception, то автоматически вызывается rollback (если нужно), для того чтобы важные финтех данные остались в консистентном виде. При успешном завершении commit тоже происходит автоматически. Конечно, внутри кода можно дополнительно управлять commit/rollback, но тут задача максимально очистить код и оставить только важную бизнес-логику, описывающую поведение продукта.

Часто бывает так, что перед обновлением записи в таблице нужно всегда дополнительно заполнить еще какие-то поля (некоторый аналог триггеров), к примеру, для денормализации или просто по логике продукта. Для этого у нас есть ряд методов, к примеру, beforeUpdate которые можно переопределить и добавить внутрь дополнительные правила.

Заполнение данных перед update в базе

Заполнение данных перед update в базе

В данном случае мы автоматически заполняем у ссуды due_date, если это поле по каким-то причинам не было задано в момент обновления записи в базе данных.

Модули и компоненты

Модуль – это минимальная единица поставки, которая независимо версионируется и состоит (опционально) из

  • Описания таблиц (в json)

  • Описания бизнес-логики (java/kotlin)

  • Набора экранов (в json) – об экранах попозже

  • Ну и еще немножко всякого разного в виде текста, типа локализации (text), документации (markdown) и прочего

Модуль может быть независимо установлен на развернутой платформе, представляет из себя один zip-файл и создается автоматически на этапе build-а проекта в IDE. Модуль — это независимая часть функционала со своими таблицами, логикой и экранами, но может быть, к примеру, модуль только с одними экранами или одними таблицами, зависит от вашего подхода к лицензированию, поставкам и архитектуры самого продукта. Экраны могут быть построены на таблицах из разных модулей, и бизнес-логика тоже может обращаться к таблицам из других модулей (если это не запрещено явно). Сама Low-Code платформа тоже представляет из себя набор системных модулей, некоторые из которых входят в дистрибутив по умолчанию, а некоторые доставляются по необходимости.

Аппликационный сервер мы построили на базе Apache Tomcat, он Stateless, ни хранит никаких данных и может быть линейно масштабирован при необходимости. Именно там «крутится» ядро Low-Code платформы, туда же «подкладываются» модули, а дальше ядро само занимается развертыванием схемы в базе данных, предоставлением API, раздачей статики для тонкого клиента (экраны строятся динамически по их описаниям в json), проверкой прав и различными диагностиками.

В качестве базовых системных модулей в ядро в том числе входят: модуль аутентификации, авторизации, аудита, работа с пользователями, конструктор экранов, базовых уведомлений, диагностики базы данных. При необходимости мы еще подключаем модуль для работы с документами (компоненты для OnlyOffice и Р7-Офис), уведомления через Telegram (с обратной связью), полнотекстовый поиск, платежи, и прочее. Количество модулей постоянно увеличивается, появляются еще разные кастомные (к примеру, интеграции с SAP, Teamcenter и прочие). В общем, тут важно сделать максимально простое подключение новых модулей, API, систему установки и поставки, чтобы потом не сойти с ума от клубка зависимостей.

Права

Для удобства раздачи прав для пользователей удобно оперировать сразу целыми экранами — выдавать права на экран, а также иметь возможность тонко настроить, что именно в конкретном экране можно и нельзя делать. Поскольку экран состоит из серверных акций (выборка, обновление таблиц, вызов дополнительных акций), то, естественно, права должны проверяться на сервере.

Для решения задач по выдаче прав на данные внутри таблицы, мы разработали специальное разделение строк в таблицах по специальному признаку «division», справочник которых заводится глобально и дает возможность раскрасить данные насквозь во всей системе. С помощью такого подхода можно некоторые таблицы оставить как глобальные справочники, единые для всех пользователей, а некоторые раскрасить, чтобы данные вообще никак не попадали в «чужие» запросы.

Диагностика

Мы столкнулись с необходимостью поддержки более тонкой диагностики работы. Нужно быстро определять состояние процессов сервера, состояние соединений сервера с базой данных, разбираться в статистике запросов к базе данных, оценивать размеры объектов и понимать статистику выполнения акций. Более того, нужно уметь по запросу сразу понимать в каком месте «кода» он вызывался. Это интересно, с учетом того, что в стандартной статистике базы этого, конечно, нет. Мы научились «прокидывать» кое-что в запросе, и удобно показывать, где и что происходит. В первую очередь, это нужно для быстрого решения проблем у клиента, чтобы сразу понять в чем дело.

Housekeeping

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

  • Временные или промежуточные данные

  • Старые записи, созданные для логирования

  • Старые данные, уже выгруженные в системы DWH (Data Warehouse), и прочие

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

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

IDE для разработки

В качестве основного IDE для разработки мы взяли IntelliJ IDEA (Community Edition), для которого написали свои собственные плагины и добавили несколько кнопок на toolbar.

Первая кнопка – по модели данных в json генерирует соответствующие классы доступа. Чтобы можно было сразу разрабатывать бизнес логику. Замечу, что если бизнес-логики еще нет (так бывает), и нужно просто сделать много экранов для заполнения данных, то не нужно ничего программировать вообще.

Вторая кнопка – собирает модуль. По сути, она тоже сначала все генерирует, но еще из проекта собирает модуль в виде zip-файла. Модуль получается абсолютно самостоятельным, и его можно сразу всем разослать, и даже закинуть на прод, но это для героев.

Третья кнопка – перезапускает локальный Tomcat с новым модулем.

В проекте хранится все необходимое для сборки модуля – описание таблиц и экранов в json, логика поведения, локализация и прочее.

Небольшой итог

В соответствии с целями, поставленными в предыдущей статье, мне нужен Low-Code инструмент для организации производства продуктов, простой и прозрачный, но без потери возможностей в сложных кастомизациях. Вся структура любого модуля «видна как на ладони»: структура базы данных в json, экраны в json, код бизнес-логики «очищен» от технической рутины и состоит преимущественно из простых функций, элементарных языковых конструкций и единообразного API к базе. При этом, никто не мешает на Java / Kotlin написать там все, что угодно. Система модульная, делится на модули в любых комбинациях и поставляется как угодно. Сервер Stateless и может быть линейно масштабируем. К базе данных привязки нет. Можно поддержать и NoSQL базы, при этом весь код будет работать, как и раньше. В коробке продумана система прав, различная аутентификация, Housekeeping и прочее, чтобы предоставить возможность разработчикам сосредоточится на разработке бизнес-решения и «забыть» про технический слой. Все обновления происходят автоматически, через UI.   

В следующей статье я расскажу, как у нас устроен тонкий клиент и редактор экранов.

Предыдущую статью можно найти тут Как мы делали Low-Code конструктор для Back Office. Часть 1

Директор по развитию ООО «Дольмен», telegram: @fintechbrother 


ссылка на оригинал статьи https://habr.com/ru/articles/840946/


Комментарии

Добавить комментарий

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