Что нам стоит дом построить? (часть 3)

febf2022f8393d5257cf82108fec3d21.jpg

Продолжаем рассказывать про 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, либо посмотреть весь список прошлых версий.

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

image-loader.svg

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

Погружаемся в связи

Поступающие к нам документы должны быть связаны между собой для облегчения их поиска и реализации различных бизнес-сценариев обработки. Мы поддерживаем 4 типа связывания:

  • raw-связи. Объект Content содержит в себе массив links, который принимает любого потомка Content (то есть вообще любой объект в системе). Мы реализовали возможность указать вид такой связи, чтобы иметь представление о том, как объекты соотносятся друг с другом. Но в общем случае это некая помойка — сюда можно затолкать всё, что угодно. Мы подразумеваем, что такой способ связывания будет использоваться в моменты переходных периодов для сохранения того, что потом будет разбираться и типизироваться в связь через атрибут;

  • связь через атрибут с объектом. В некоторых процессах нам нужно ссылаться на весь объект вне зависимости от его текущей версии. Для примера можно привести отношение клиент-договор. То есть, как бы не изменились первый или второй объект, между ними всегда будет отношение.

  • связь через атрибут с версией объекта. Но бывают ситуации, когда отношение между двумя объектами нужно привести к отношению между конкретными представлениями. Для примера — отношение договор-паспорт клиента всегда опирается на версии. Оператор может сознательно указать, что в отношении нужно использовать самую свежую версию любого из объектов, но в общем случае фиксируются состояния.

  • связь через атрибут с каскадом объектов. Этот способ связывания определяет отношения с иерархическими справочниками. В качестве примера: документ СТС и справочники марок и моделей автомобилей в нем.

Познаем нативность атрибутов и моделей

Кроме настройки объекта через типизатор, о чем я говорил выше, мы поддерживаем возможность создания модели в java-коде. Схема с конфигурированием при всем кажущемся удобстве несет ряд недостатков, главный из которых, как ни странно, и есть то самое удобство конфигурирования.

Как я уже говорил, система хранения со временем будет обрастать различными бизнес-процессами обработки хранимых документов. Далеко не всех, но какой-то значимой части точно, например, процессы интеграции с системами ЮЗ ЭДО, сканирования документов и т.п. Это влечет за собой необходимость закрепить некоторые атрибуты и создать в коде понятную модель их обработки, без необходимости врукопашную разбирать json.

Гибкая настройка позволяет еще и удалять атрибуты, из-за чего неаккуратной настройкой можно хорошенько что-нибудь сломать. Компенсаторной мерой может выступить перекрестная валидация изменений аналитиками, архитекторами и разработчиками перед их внесением, но это убивает гибкость. Поэтому в ряде случаев мы сознательно описываем модели именно как java объекты, не ограничиваясь json схемами.

Но две схемы конфигурирования нужно свести в одну, поэтому мы аннотируем такие модели и их атрибуты в коде, а потом склеиваем с заданными в базе данных — и всё то, что «прибито гвоздями», получает отметку о нативности и более не доступно к удалению или модификации идентификатора. Все остальное — маски, валидаторы, размерности, псевдонимы — остается доступным для изменения.

Более того, если по какой-то причине мы откажемся от использования hardcode по этой модели, ее описатель все равно останется в типизаторе, и объекты такого типа можно будет загружать в систему так же, как и раньше. При этом откроется возможность редактирования ранее закрытых атрибутов.

Продолжение читайте в следующей статье.

© Habrahabr.ru