Детектив NoSQL: как мы отслеживаем изменения данных в Банке Идей

e4ef7e5eb89b2daded20a39eb0b7dc32.jpg

Представьте, что вы возвращаетесь домой и замечаете, что кто-то съел ваш ужин или полежал на вашей кровати. Не нравится? Вот и владельцам информационных систем не нравится, когда они не могут понять, кто же хулиганит в их бизнес-процессах. Меня зовут Светлана Мелешкина, и я ведущий разработчик Банка Идей НЛМК. Именно в Банке Идей иногда происходили такие детективные истории.

Банк Идей — это уникальная внутренняя система, где любой сотрудник может поделиться своими предложениями для улучшения производственных процессов или условий труда. Во время работы с этой ИС мы сталкивались с постоянными вызовами в отслеживании изменений данных. Нам нужен был простой и доступный для сотрудников инструмент. Для этого мы решили использовать MongoDB — мощное решение в мире NoSQL.

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

Что такое Банк Идей?

Банк Идей — это внутренняя информационная система НЛМК. В ней пользователь может внести свое предложение (идею), улучшающее производственные процессы или условия труда. Сотрудники по итогу рассмотрения и внедрения своих идей получают премии и улучшения на рабочих местах. НЛМК получает улучшенные производственные процессы и возможность для сотрудников проявить инициативу. Win-win стратегия в действии. Путь, который проходят идеи, различен для разных типов инициатив. Для технически сложных идей путь может быть долог, но и вознаграждение, рассчитанное по экономическому эффекту идеи, будет весьма весомым. Для идей, направленных на улучшение рабочего места сотрудника, этот путь быстр, изменения на рабочем месте появятся без особых задержек, и вознаграждение за проявленную инициативу тоже будет. 

Все это мы автоматизируем, формализуем и храним в базе данных. Основной UI Банка идей представлен сайтом, работающим на ASP .Net 5. Работу сайта и обработку данных обеспечивает бекенд, работающий на .NET 5. Хранение данных обеспечивает БД MSSQL. Кодовая база, как обычно и бывает в подобных больших проектах, далека от идеала, зачастую добавление нового функционала — путь, полный страданий. 

Наша боль 

Бизнес логика работы с идеями довольно сложная. Рассмотрение идеи проходит через различные статусы, при этом меняются или добавляются различные данные, и автоматически рассчитывается или пересчитывается вознаграждение, поскольку некоторые пути прохождения идеи предусматривают несколько выплат. В общем-то вопрос про выплаты был самым частым запросом службы поддержки. И, такое случалось, что мы не могли дать ответ об изменениях, повлиявших на выплату. К тому же расследование каждого такого запроса требовало привлечения разработчиков. А если данные были удалены, то следов этой операции вообще не оставалось. И так у нас назрела необходимость вести журнал изменений, который могли бы просматривать специалисты поддержки. Причем должна быть возможность одним запросом вытащить лог изменений по одной идее, и по нему уже вести расследование, а не просматривать несколько sql таблиц в поиске всех изменений.

Формулируем задачу

  1. Объем задачи — пишем все изменения данных по списку (около 50 точек журналирования, 35 разных типов данных).

  2. Легкость поиска — Нужна легкость поиска данных и их расшифровки.

  3. Доступность для бизнеса — конечным пользователем будет бизнес-администратор ИС и процесс работы с журналом должен быть простым и лаконичным.

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

  5. Срочность — По возможности ввести в эксплуатацию еще вчера, система активно используется, и каждый день, когда мы не пишем журнал, мы что-то теряем.

Выбор технологии

Казалось бы, если мы уже знаем все типы данных, которые имеют потребность к журналированию (Идея, Авторы, Тех. Советы, Выплаты, Задачи, Руководитель проекта и тд.), то почему нам не сделать журнал на основе MS SQL и не внедрять новых решений?
Для систем такого масштаба как наш Банк идей, короткого ответа на такой вопрос не будет, все складывается из «Мелочей», давайте разбираться.


Первое, что приходит в голову — «Если мы уже знаем, с какими типами данных нам предстоит работать, то просто немного доработаем нашу БД». У этого подхода есть достаточно большие минусы.

Подход 1. «В лоб»

Мы можем создать таблицу для журнала и объединить в ней изменения всех сущностей, но как описано выше, таких типов на данный момент 35 и общего у них только Id идеи (в основном). Если идти по такому варианту, то мы получим огромную таблицу, с которой банально не удобно работать, интуитивно будет невозможно понять, что там происходит и новому человеку нужно будет много времени для понимания. Второй вариант из категории «В лоб», это создать 35 таблиц, но это создаст сложность там, где она не нужна — сквозной поиск по 35 таблицам не простая задача.

Подход 2. «Первая проба в гибкость»

Другой вариант, это использовать json, для этого мы можем попробовать создать одну таблицу, одним из полей которой будет строка, в которую будет записан json изменений. Поиск осуществляем с помощью SQL функции Like. С этим подходом есть сразу несколько проблем, а именно:

  • Нет возможности прогнозировать размер json«a. В связи с этим возможна деградация скорости поиска, создание индексов, теоретически, может не представляться возможным из-за объема.

  • Like уже нас не спасет, поскольку мы хотим осуществлять поиск не только например по id, но и по диапазону дат в этом json       

Подход 3. «Вторая проба в гибкость»

Изучив вопрос более подробно, можно обнаружить, что в MS SQL есть технология Json колонок. Эта технология позволяет выполнять поиск по внутренним полям.

Рисунок 1. Пример запроса по json колонке в MS SQL.

Рисунок 1. Пример запроса по json колонке в MS SQL.

Казалось бы, вот оно решение, зачем нам эта MongoDB? Не все так просто.

·        Запрос к такой таблице все еще происходит в формате SQL, объективно, составлять SQL запросы не тривиальная задача для конечного пользователя этого продукта — специалиста технической поддержки.

·        Формат получаемого результат в SQL management studio не удовлетворяет требования доступности для бизнеса, придется каждый раз доставать json и конвертировать его сторонним ПО

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

·        Management studio имеет ограничения по длине копируемого из ячейки текста в буфер, а также на длину выгружаемой в csv строки.

Вишенкой на торте станет потребность создать индекс по этой таблице для быстрого поиска, а делается это также через SQL.

Рисунок 2. Пример запроса для создания индекса.

Рисунок 2. Пример запроса для создания индекса.

На данный момент мы имеем 3 варианта решения задачи, два из которых совершенно не состоятельны для нашей задачи и один просто не удобный. Тут к нам на помощь приходит NoSQL СУБД — MongoDB.

Подход 4. «Забивай гвозди молотком»

MongoDB сочетает в себе все плюсы третьего подхода и, если не решает все его проблемы, то хотя бы смягчает их.

  • Не надо использовать специфические типы столбцов, MongoDB работает с json из коробки

  • Для человека далекого от разработки ПО, поиск нужной информации более прозрачен. {Название поля: "Значение”}

    Рисунок 3. Пример запроса к таблице в MongoDB.

    Рисунок 3. Пример запроса к таблице в MongoDB.

  • Бесплатный инструмент работы с MongoDB (MongoDB Compass) сразу возвращает форматированный ответ, нет потребности обращаться к другим инструментам. Его можно спокойно дать специалисту технической поддержки, и он сможет получать всю необходимую информацию без помощи разработчиков.

  • ·Индекс создается в два клика через UI и может быть добавлен рядовым пользователем

    Рисунок 4. Пример запроса для создания индекса.

    Рисунок 4. Пример запроса для создания индекса.

Таким образом, по совокупности факторов было решено остановиться на реализации через MongoDB

Воплощение в жизнь

Первая реализация была сделана очень быстро, просто сериализовали команду на изменение, получаемое бекендом, и раскидали вызовы записи в журнал по нужным точкам логирования, добавив в каждой подробности об имени карточки и производимом действии. Запустив эту систему на тестовом окружении, мы поняли, что местами данных мало. Например, не достаточно сохранить Id удаленной записи, нужно больше подробностей. Пришлось выписать списком все карточки журналирования, которые нам не нравились, и поработать с ними отдельно. Где-то как в случае с удалением добавили сохранение удаленных данных, где-то просто добавили данных для человеко-читаемости, поскольку первая итерация журналирования предполагалась без отдельного UI для саппорта, задача уже горела так, что запустить ее в реальную работу хотелось побыстрее. Что в общем-то нам и удалось, в production задача была запущена после двух недель разработки, с тех пор крутится, помогая нам расследовать обращения в саппорт.

Примеры фильтров MongoDB

Принцип их построения фильтров отличается от SQL принципов, которые разработчики обычно изучают как базу. Для тех, кто уже работал с Elastic Search и подобными системами, сюрпризов быть не должно. Агрегация данных и возможности построения статистики на этих данных тоже имеются, но выходят за рамки данной статьи.

Итак, простые фильтры, написанные по запросам саппорта:

  • Все изменения идеи 123456: {ChangedObjectId: 123456}

  • Что менял пользователь с табельным 00111111: UserTN: "00111111"}

  • Все изменения авторов идеи 123456: {ChangedObjectId: 123456, Card:"Authors"}

Пример немного посложнее с использованием функций. У нас есть своя система ролей для пользователей, эти роли мы меняем и храним внутри Банка Идей, поэтому также пишем в журнал список добавленных или отобранных у пользователя ролей. Для важных системных ролей есть необходимость держать руку на пульсе, и видеть, когда и кто их добавлял. В терминологии монго мы делаем запрос с проверкой вхождения id искомых ролей в массив добавленных ролей. 

{Card:"UserRoles", "Value.AddedRoleIds": {"$in": [3, 4, 8]}}

К любому фильтру, можем добавить фильтрацию по дате с помощью функций $gt, $gte, $lt, $lte. 

Что мы получили, выбрав MongoDB для журнала изменений:

  • Быстрая разработка и внедрение. Фактически раз-два и в production.

  • Простота добавления к журналу новых точек для снятия данных. Обеспечивается гибкостью данных.

  • Гибкость формата данных обеспечивает так же легкое добавление новых индексов.

  • Невысокий порог входа для поиска данных по журналу без разработки отдельного UI для специалистов службы поддержки. Но у нас на поддержке достаточно квалифицированные люди.

Немного деталей кода и фильтров

Для MongoDB есть родной Nuget пакет MongoDB.Driver для работы с базой. Для просмотра данных использовали MongoDB Compass.

Реализация слоя доступа к данным. Так выглядит наш объект для хранения в базе:

[Serializable]
public class ChangeEntity
{
  [BsonId]
  [BsonRepresentation(BsonType.ObjectId)]
  public string Id { get; set; }

  public int ChangedObjectId { get; set; }

  public string UserTN { get; set; }

  public string Action { get; set; }

  public string Card { get; set; }

  public BsonDocument Value { get; set; }

  public DateTime ChangeDate { get; set; }
}

Для сквозного поиска по истории изменений в общие поля вынесены ключевые данные:

  • id изменяемого объекта

  • id пользователя

  • действие

  • место действия

  • дата изменения

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

{ChangedObjectId: 123456}

Подобный фильтр мы легко и просто можем отдать службе поддержки для работы с журналом. Если нам, например, нужно уточнение выборки по дате, то фильтр выглядит вот так:

{ChangedObjectId: 123456, ChangeDate: {$gt: ISODate('2024-05-01')}}

Сложнее, но вполне доступно для работы, особенно если описать запросы с примерами на Confluence. Конечно, лучше бы написать отдельную страничку с UI для саппорта, но пока до нее дойдут руки разработчиков, это вполне рабочий способ отдать просмотр журнала на сторону саппорта.

Следующим реализуем репозиторий для записи в журнал. Хочу заметить, таблиц как в SQL тут нет, база данных содержит коллекции. В нашем случае коллекция одна — журнал. Запись в журнал выглядит так:

public async Task AddEntry(
  AddChangeCommand command,
  CancellationToken cancellationToken)
{
    var entry = _mapper.Map(command);
    entry.ChangeDate = DateTime.Now;

    await _journalCollection.InsertOneAsync(entry, cancellationToken);

    return entry.Id;
}

Как видно по коду, для простоты изоляции DAL слоя от слоя бизнес логики тут подключен AutoMapper. Маппинг из данных команды записи в журнал в запись журнала выглядит предельно просто:

public MappingProfile()
{
  CreateMap()
    .ForMember( x => x.Value, opt => opt.MapFrom(x => BsonDocument.Parse(x.Value)));
}

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

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

Магия! Фильтры, которые будут учитывать внутренние данные, будут отбирать только записи, в которых эти данные есть, что вполне нас устраивае в момент, когда мы хотим увидеть только журнал определенного действия. Фильтры, конечно, очень далеки от MSSQL запросов, но зато дают возможность собрать в кучу разрозненные по типам данные.

А вот так выглядит место вызов для записи в журнал. У нас везде используется медиатор, поэтому этот вызов сделан как обработчик команды AddJournalEntryRequest:

var jsonSetting = new JsonSerializerOptions
{
  IgnoreNullValues = true,
  Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

var value = JsonSerializer.Serialize(request.Value, jsonSetting);

var cmd = new AddChangeCommand
{
  UserTN = userTN,
  ChangedObjectId = request.ChangedObjectId,
  Action = request.Action,
  Card = request.Card,
  Value = value,
};

await _changesJournalRepository.AddEntry(cmd, cancellationToken);

А вот одна из точек записи в журнал:

await _mediator.Publish(new AddJournalEntryRequest
{
  ChangedObjectId = link.OfferId,
  Action = "Update",
  Card = "OfferLinks",
  Value = new
  {
    LinkTo = link.LinkToOfferId,
    LinkType = linkTypeName
  }
});

Выводы

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

Если ваши данные не имеют жесткой структуры и не связаны между собой, NoSQL подойдет для их хранения лучше традиционной реляционной базы данных. Использование MongoDB для журнала изменений Банка Идей принесло значительную пользу и решение оказалось эффективным по следующим критериям:

  • Гибкость хранения данных: MongoDB обеспечивает гибкий формат хранения данных, что позволяет легко добавлять новые типы данных без изменений в структуре базы данных. Это особенно важно для проектов со сложной бизнес логикой.

  • Простота поиска данных: MongoDB обладает интуитивно понятными запросами к базе данных, что облегчает поиск и анализ данных. Это особенно важно для службы поддержки, которая может оперативно проводить расследования по данным журнала.

  • Расширяемость: MongoDB обеспечивает возможность легкого расширения системы и добавления новых типов данных без необходимости значительных изменений. Это позволяет быстро реагировать на изменения в бизнес-процессах и требованиях.

Итак, выбор MongoDB для журнала изменений данных в проекте «Банк Идей» НЛМК оправдал себя и позволил эффективно решать задачи службы поддержки. Гибкость, простота использования и масштабируемость делают NoSQL базы данных привлекательным решением для проектов с динамичной и сложной структурой данных.

Что почитать по теме

В этой статье я показываю, как мы использовали NoSQL для нашей задачи. Возможно, у читателей появились вопросы как выбирать базу данных для своих задач. Очень рекомендую книгу Мартина Клеппмана «Высоконагруженные приложения. Программирование, масштабирование, поддержка» (книга с кабанчиком). Несмотря на название, эта книга именно про базы данных для высоконагруженных систем, а не непосредственно сами системы. Полезно не только для разработчиков Highload, но и обычным разработчикам для расширения кругозора в области работы с данными.

© Habrahabr.ru