Продолжаем рассказывать про ECM-систему, разработкой которой мы занимаемся. С момента выхода предыдущей статьи прошло достаточно много времени, и мы успели не только создать прототип, но и реализовать немалое количество функционала. Сейчас мы готовы выйти в опытную эксплуатацию, поэтому все, что описано ниже — это то, что в итоге у нас получилось.
Говорим о компонентах и не только
Об этом расскажу я, Александр Болдырев, техлид разработки в Блоке ИТ-развития корпоративного бизнеса РСХБ-Интех. Напомню, что мы создаем микросервисную систему, но описывать каждый микросервис — занятие неблагодарное, а для людей, непричастных к разработке — даже вредное, так как велика вероятность их только запутать. Поэтому при описании функционала системы мы обычно оперируем более общим понятием «сервис», которое объединяет несколько микросервисов и инкапсулирует в себе всю логику работы определенного слоя системы.
Сейчас у нас практически полностью реализована та часть системы, которую мы относим к слою «ядро» — функционалу, который переиспользуется во всех остальных частях системы. Эту часть мы обозначаем как «платформа», и далее я буду часто оперировать этим термином.
Платформа состоит из следующих сервисов:
Объектный. Оперирует всем слоем работы с бизнес-атрибутами контента, позволяет управлять их составом, предоставляет механизмы версионирования, определяет порядок работы с базой данных. Фактически этот сервис оборачивает собой работу с Postgre.
Медиа-сервис. Описывает весь функционал работы с файлами, управляет расположением хранилища, перемещает файлы между хранилищами. Этот сервис в нецелевом решении работает с GridFS MongoDB, в целевом — с создаваемой у нас SDS по протоколу S3.
Поисковый. Это — индексация объектов, быстрый поиск, работа с поисковыми операторами (больше, меньше), сортировка по полям, кросс-индексный поиск (например, когда нам нужно найти все объекты типа В, связанные с объектом А). Здесь идет работа с Elasticsearch.
Права доступа. В нашей системе можно разграничивать доступ по типу объекта и дереву его наследников, по полям объекта, по функциям в интерфейсе. Планируется, что в нашей IDM-системе будут храниться группы пользователей, на основании которых мы будем формировать матрицы доступа.
Шлюз. Как я уже описывал в прошлых статьях, шлюз реализует задачи предоставления интерфейса для пользователя и преобразования http-запросов от клиента в работу с внутренним брокером Apache Kafka; группировка микросервисов в сервисы как раз и отражается здесь в виде деревьев методов: /api/object — маршрутизирует запрос на объектный сервис, /api/flie — на медиа-сервис. Естественно, что в процессе работы понадобятся кросс-сервисные запросы и это также реализовано здесь. При первом взгляде может показаться, что шлюз — это явная точка отказа, но он, как и любой другой микросервис, также представлен в виде отдельного контейнера в k8s и точно так же может быть масштабирован.
Подробное описание всех сервисов — слишком объемная тема для одной статьи, поэтому я сегодня буду говорить только о самом большом и сложном из них.
Работаем с объектным сервисом
Как я уже говорил, мы храним объекты в базе данных в виде JSON-структур и целевым решением для хранения выбран Postgre. Для того, чтобы научить систему работать с разными типами объектов, мы предусмотрели справочник «Типизатор моделей».
Модель в нашем случае — это описатель конкретного типа документа, который содержит в себе:
псевдоним модели, ее внутренний код;
информацию о родительской модели. Все без исключения типы моделей в нашей системе унаследованы от базовой модели Content (потому что Object уже занято): она описывает совсем базовые поля, вроде пользователя, создавшего объект, различных штампов времени и некоего нетипизированного массива связей объекта. Каждый потомок расширяет базовую модель, привнося свои атрибуты, но при этом сохраняя неизменным набор атрибутов родителя. До модификаторов доступа (private, public и т.п.) мы не спускались, но такую возможность на будущее заложили.
данные об атрибутах модели, каждый из которых — это ключ в JSON-структуре. Мы даем возможность задать каждому из атрибутов его код и псевдоним; тип данных — как классические int, string, date, так и ссылочные — единичные и множественные ссылки на другие объекты системы; настройку видимости поля в интерфейсе; маску ввода и regexp выражение валидации введенного значения; минимальную и максимальную длину вводимых данных — в общем всё то, что позволит работать с полем в интерфейсе.
Кроме базовой модели Content мы дополнительно реализовали 2 дерева, которые сами по себе не содержат атрибутов, но служат общим родителем. Это нужно, чтобы иметь возможность быстро привести объект к какому-нибудь из базовых типов и понять, с чем именно мы работаем. Родитель Document является родителем для любого документа, но не объекта. Например, паспорт — это наследник от Document, а клиент, к которому паспорт привязан, — нет. Извлекая «Клиента», мы видим, что в его связях — документы, а что — другие объекты. Родитель Dictionary — это справочники, они у нас динамические не только в части введения новых значений, но и в части создания самих справочников и последующей привязки их к моделям объектов.
После описания модели она сразу же становится доступной для заполнения в интерфейсе системы, который строится на лету. Для целей интеграции можно экспортировать json-схемы объектов, заполнив которые сторонняя система сможет сразу начать сохранять у нас свои файлы.
На входном шлюзе мы валидируем поступающие данные по нашим правилам и возвращаем пользователю ошибку в случае, если валидация не прошла. При этом в тексте ошибки мы сразу указываем, что именно ее вызвало. Кроме того, каждый поступивший документ проходит процедуру контроля на повторение: по набору ключевых атрибутов (которые также можно настроить) мы проверяем, есть ли в хранилище такой объект.
Отсюда возможно два пути:
создание нового объекта. Это самый очевидный и не требующий комментариев результат;
повышение версии объекта и прикрепленного к нему файла. Тут надо рассказать подробнее.
Изучаем версионирование
Один из наиболее часто возникающих вопросов — это описание модели версионирования. В каждом объекте у нас есть два идентификатора — ID объекта и ID версии объекта. При сохранении новой версии происходят 2 действия: текущей версии устанавливается признак «Архивная» и создается новая запись с новым ID версии. Это конфигурируемый порядок, потому что для некоторых объектов версионирование можно выключить целиком или для конкретных полей объекта. В общем случае после сохранения мы получим два объекта, сгруппированных под одним ID объекта. Доступ к прошлым версиям можно легко получить по их ID, либо посмотреть весь список прошлых версий.
Файлы также могут быть привязаны как к версии объекта, так и к объекту целиком, что определяет порядок их сохранения:
Версионирование необходимо не только для сохранения истории, но и для реализации статических и динамических связей между объектами.
Погружаемся в связи
Поступающие к нам документы должны быть связаны между собой для облегчения их поиска и реализации различных бизнес-сценариев обработки. Мы поддерживаем 4 типа связывания:
raw-связи. Объект Content содержит в себе массив links, который принимает любого потомка Content (то есть вообще любой объект в системе). Мы реализовали возможность указать вид такой связи, чтобы иметь представление о том, как объекты соотносятся друг с другом. Но в общем случае это некая помойка — сюда можно затолкать всё, что угодно. Мы подразумеваем, что такой способ связывания будет использоваться в моменты переходных периодов для сохранения того, что потом будет разбираться и типизироваться в связь через атрибут;
связь через атрибут с объектом. В некоторых процессах нам нужно ссылаться на весь объект вне зависимости от его текущей версии. Для примера можно привести отношение клиент-договор. То есть, как бы не изменились первый или второй объект, между ними всегда будет отношение.
связь через атрибут с версией объекта. Но бывают ситуации, когда отношение между двумя объектами нужно привести к отношению между конкретными представлениями. Для примера — отношение договор-паспорт клиента всегда опирается на версии. Оператор может сознательно указать, что в отношении нужно использовать самую свежую версию любого из объектов, но в общем случае фиксируются состояния.
связь через атрибут с каскадом объектов. Этот способ связывания определяет отношения с иерархическими справочниками. В качестве примера: документ СТС и справочники марок и моделей автомобилей в нем.
Познаем нативность атрибутов и моделей
Кроме настройки объекта через типизатор, о чем я говорил выше, мы поддерживаем возможность создания модели в java-коде. Схема с конфигурированием при всем кажущемся удобстве несет ряд недостатков, главный из которых, как ни странно, и есть то самое удобство конфигурирования.
Как я уже говорил, система хранения со временем будет обрастать различными бизнес-процессами обработки хранимых документов. Далеко не всех, но какой-то значимой части точно, например, процессы интеграции с системами ЮЗ ЭДО, сканирования документов и т.п. Это влечет за собой необходимость закрепить некоторые атрибуты и создать в коде понятную модель их обработки, без необходимости врукопашную разбирать json.
Гибкая настройка позволяет еще и удалять атрибуты, из-за чего неаккуратной настройкой можно хорошенько что-нибудь сломать. Компенсаторной мерой может выступить перекрестная валидация изменений аналитиками, архитекторами и разработчиками перед их внесением, но это убивает гибкость. Поэтому в ряде случаев мы сознательно описываем модели именно как java объекты, не ограничиваясь json схемами.
Но две схемы конфигурирования нужно свести в одну, поэтому мы аннотируем такие модели и их атрибуты в коде, а потом склеиваем с заданными в базе данных — и всё то, что «прибито гвоздями», получает отметку о нативности и более не доступно к удалению или модификации идентификатора. Все остальное — маски, валидаторы, размерности, псевдонимы — остается доступным для изменения.
Более того, если по какой-то причине мы откажемся от использования hardcode по этой модели, ее описатель все равно останется в типизаторе, и объекты такого типа можно будет загружать в систему так же, как и раньше. При этом откроется возможность редактирования ранее закрытых атрибутов.
Продолжение читайте в следующей статье.