В настоящее время объектно-ориентированные базы данных (ООБД) не имеют достаточно большого распространения в повседневном использовании, да и более того, не настолько популярны как реляционные базы данных, которые не один десяток лет уже активно поддерживаются различными сообществами и имеют долгую историю применения.
В данной статье рассматривается реализация ООБД в контексте разработки системы, состоящей из микросервисов, на примере Perst и Db4o. Также будет рассмотрена отдельная реализация с документно-ориентированной базой данных MongoDB, работа с которой имеет много общего с ООБД.
Целью данной статьи является рассмотрение практического применения ООБД и решения проблем совместимости с помощью микросервисной архитектуры.
![](https://habrastorage.org/getpro/habr/upload_files/715/1d8/30f/7151d830f1da47600b5b6b8ce88f33ba.png)
Введение
При написании данной статьи я руководствовался личным опытом разработки микросервисов и работы с ООБД на языке C#, с помощью которого будут продемонстрированы все примеры. Некоторые темы связанные с микросервисной архитектурой включая декомпозицию бизнес логики системы, предметно-ориентированное проектирование и решение проблематики согласованности данных между сервисами рассмотрены не будут - рассмотрение данных тем читателю предлагается осуществить самостоятельно.
В разработанной системе будет представлено три сервиса, каждый из которых взаимодействует со своей базой данных и реализует основные CRUD-операции с коллекциями объектов. Также, каждый сервис по своему справляется с решением проблемы внутреннего и внешнего представления данных.
В данной работе представлен так называемый "распределённый монолит", поскольку все единицы модульности (сервисы) зависят друг от друга.
Под внутренним представлением данных я понимаю множество моделей, которые используются с конкретной ООБД. Моделью в рамках текущей статьи, будем считать класс, который содержит атрибуты, несущие смысловую нагрузку в рамках конкретной коллекции объектов.
Пример модели
namespace oodb_project.models
{
/// <summary>
/// Модель, характеризующая источник данных для сервиса
/// </summary>
public class DataSourceModel : IdModel
{
public DataSourceModel() : base()
{
}
public DataSourceModel(string? id, string? name, string? url) : base(id)
{
Name = name;
Url = url;
}
/// <summary>
/// Имя ресурса
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Ссылка на ресурс
/// </summary>
public string? Url { get; set; }
}
}
Под внешним представлением данных я понимаю множество моделей, которые являются результатом выполнения запросов. В дальнейшем эти моменты будут определены.
Кто определяет общий формат выходных данных
Общий формат выходных данных для каждого сервиса определяет главный сервис - oodb-main-server. Выходные данные - это результат выполнения запроса конкретным сервисом (oodb-mongo-server или oodb-perst-server).
Взаимодействие с коллекциями объектов каждой конкретной ООБД будет рассмотрено на основе реляционного подхода (используемый в классических реляционных базах данных), как и проектирование этих коллекций.
Данная статья не является полноценным руководством по использованию таких баз данных как MongoDB, Perst и db4o. По большей части в данной статье происходит описание уже реализованного функционала, в котором происходит работа со всеми перечисленными базами данных.
Все ссылки на открытые исходники кода для каждого сервиса предоставляются своевременно, сразу после описания полученных результатов. При желании читатель может обратиться к этим исходникам кода и ознакомиться с системой подробнее. Все репозитории снабжены достаточным количеством комментариев для понимания работы кода. Для работы всей системы необходимо настроить контейнеры для базы данных MongoDB. В данной статье рассматривается настройка этих контейнеров.
Предполагается, что при настройке MongoDB, подключение Perst и db4o к каждому конкретному сервису согласно рекомендациям, перечисленным в данной статье читатель сможет самостоятельно воспроизвести результаты данной работы (запустить систему).
Описание предметной области
Предметной областью в данной статье выступает система веб-сервиса мониторинга удаленных хостов, с возможностями развёртывания на хостах определённых приложений.
При достижении определенного этапа разработки системы появляется задача развёртывания элементов (или модулей) на удалённые хосты, чтобы пользователи могли работать с каждым из них (как правило, этот момент скрывается за слоем реализации).
Отмечу, что в данный момент существует большое множество инструментов, которые помогают решить большинство проблем связанных с непрерывной доставкой, интеграцией, автоматическим тестированием и мониторингом удалённых хостов. Например, CI/CD, Zabbix, Docker, Kubernetes, Shef, Ansible, NixOS и многие другие.
Однако, будем полагать, что стоит задача разработать новую систему решающие те же проблемы, что и известные аналоги. Для этой системы нужно спроектировать базу данных и разработать сервисы.
Проектирование модели базы данных
Спроектированная модель базы данных будет использоваться на каждом сервисе. В некотором смысле данная модель будет "идеальной" моделью, к которой каждая база данных на сервисах будет стремиться.
Начнём с описания сущностей, которые в данной системе будут активно использоваться.
Основными сущностями, не считая связей между ними, будут следующие:
Хосты (hosts) - это конкретные сервера, на которых работают какие-либо приложения (сервисы).
Сервисы (services) - это конкретное приложение, которое может быть запущено на любом хосту.
Ресурсы (data_sources) - это ссылки на ресурсы, которые используются сервисом для осуществления своей работы (например, ссылка на GitHub-репозиторий).
Админы (admins) - это администратор системы, которому доступен просмотр технических характеристик хоста (мониторинг хоста).
Журнал мониторинга (monitor_apps) - это журнал, в котором фиксируется каждый конкретный администратор и закреплённый за ним хост, чтобы каждый админ мог мониторить только ему доступные хосты.
Каждая сущность определяется своим набором атрибутов, которые позволяют описать её как объект в контексте объектно-ориентированного подхода. Атрибуты каждой отдельной сущности и отношения между всеми сущностями представлены на рисунке 1.
![Рисунок 1 - Модель базы данных Рисунок 1 - Модель базы данных](https://habrastorage.org/getpro/habr/upload_files/645/bed/93f/645bed93fafc5c359cd79b4399dd610c.png)
В модели базы данных обозначены атрибуты для каждой сущности, с которыми можно ознакомиться в таблицах.
Дополнительно схему базы данных сопровождают отношения между сущностями, количество которых достаточно для осуществления разнопланового тестирования системы: от реализации простых CRUD-операций, до реализации каскадного удаления.
Проектирование архитектуры системы
Что такое микросервисная архитектура? Микросервисная архитектура - это стиль проектирования, который разбивает систему на отдельные сервисы с разными функциями.
Единицей модульности в данном стиле проектирования выступает сервис.
Каждый сервис имеет свою базу данных, в которых находятся все важные данные, используемые при выполнении бизнес-логики.
В случае данной системы сервисы обладают разными базами данных, которые воспроизводят одну и ту же реляционную схему. Всего в системе три сервиса.
Схема спроектированной архитектуры представлена на рисунке 2.
![Рисунок 2 - Архитектура системы Рисунок 2 - Архитектура системы](https://habrastorage.org/getpro/habr/upload_files/00d/4f6/9fd/00d4f69fdd8af9fd2fa4879a4bf9ee2b.png)
Из рисунка 2 можно сделать вывод, что в дополнении к трём сервисам в системе были добавлены desktop-клиентское приложение и два контейнера, один из которых для работы с базой данных, а другой - для работы с MongoDB через веб-клиент.
Кстати, на рисунке изображены также пояснения к технологиям, которые были использованы в текущей системе.
Вообще, микросервисная архитектура хороша тем, что каждая отдельная единица модульности может быть разработана с использованием разных технологий - от Node.js, Gin-Gonic, Nest.js до Spring Boot, Laravel и т.д. Я считаю это одним из достоинств данной архитектуры.
В рамках текущей статьи я рассчитываю раскрыть читателю практическую пользу данного достоинства избранной архитектуры при решении одной не совсем очевидной проблемы.
Описание проблемы, которая решается с помощью данной архитектуры
При разработке сервиса для работы с базой данных Perst хотелось бы использовать ASP Net Core и более новую версию C#, т.к. технологии развиваются и не хочется "отставать" от общего потока развития. Однако, всё не так просто. Если сервисы для работы с MongoDB и db4o были разработаны с помощью .NET 6 и ASP NET Core, то с этим сервисом пришлось сделать "откат" технологий до .NET Framework 4.8 и использовать несколько иные подходы к проектированию работы приложения, потому что Perst не работает с новыми версиями .NET (не поддерживает). В качестве доказательства существования этой проблемы предлагаю читателю самостоятельно попробовать подключить Perst к более новым версиям .NET.
Разработка сервиса для работы с MongoDB (oodb-mongo-server)
Настройка базы данных
Описание практической разработки системы будет начато с сервиса, который взаимодействует с документно-ориентированной базой данных. Будем двигаться "сверху вниз" согласно архитектуре, представленной на рисунке 2.
В начале необходимо развернуть базу данных MongoDB в контейнере, для взаимодействия с ней. Это необходимо, чтобы локально работать с данной базой без дополнительных инсталляций.
Я использовал docker-compose для развёртывания контейнера mongo, а также mongo-express для возможности использовать веб-интерфейс, взаимодействующей с MongoDB.
Следующие инструкции в docker-compose настраивают автоматическое локальное развёртывание описанных выше контейнеров:
version: '3.1'
services:
mongo:
image: mongo
restart: always
ports:
- 27017:27017
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
mongo-express:
image: mongo-express
restart: always
ports:
- 8081:8081
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: root
ME_CONFIG_MONGODB_ADMINPASSWORD: example
ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo:27017/
depends_on:
- mongo
Здесь используется версия 3.1, описание состоит из двух сервисов: mongo и mongo-express.
В image указан образ, из которого будет собран контейнер. Если не указывать версию образа он будет загружен из Docker Hub согласно последней версии (она же является версией по-умолчанию).
Каждый из сервисов автоматически перезапускается, что обозначается как restart: always
В environment определяются значения конфигурации для подключения к базе данных (mongo-express) и инициализация паролем и логином базы данных для взаимодействия с ней (mongo).
Порты указаны в ports, а чтобы mongo-express запустился после mongo и был зависим от данного контейнера был указан depends_on в mongo-express.
Для запуска контейнеров, описанных в docker-compose, необходимо их сначала собрать используя команду docker-compose build
, а затем запустить командой docker-compose up
.
![Рисунок 3 - Сборка контейнеров с помощью docker-compose Рисунок 3 - Сборка контейнеров с помощью docker-compose](https://habrastorage.org/getpro/habr/upload_files/3bd/ed1/a7e/3bded1a7e29354a1cc02ac3f9733e46a.png)
При запуске всех контейнеров они должны отображаться в списке контейнеров. Для этого удобно использовать команду docker container ps -a
![Рисунок 4 - Активные контейнеры Рисунок 4 - Активные контейнеры](https://habrastorage.org/getpro/habr/upload_files/b46/bb7/92a/b46bb792a79c8569bd275dd702604edd.png)
После того, как контейнеры были развёрнуты следует проверить подключение к MongoDB с помощью веб-интерфейса. В случае текущих настроек, которые были определены в docker-compose, путь к веб-интерфейсу определён по адресу http://localhost:8081
![Рисунок 5 - Веб-интерфейс для работы с MongoDB Рисунок 5 - Веб-интерфейс для работы с MongoDB](https://habrastorage.org/getpro/habr/upload_files/bd9/9b4/c80/bd99b4c80a196f3a7a374f46dce40c24.png)
При работе с веб-интерфейсом необходимо создать базу данных oodb (я это уже сделал), и в данной базе данных создать следующие коллекции: AdminList, DataSourceList, HostList, HostServiceList, MonitorAppList, ServiceList.
И при такой настройке MongoDB исходный код, который будет представлен в конце статьи, будет работать со средой тех читателей, которые решат попробовать запустить исходный код.
![Рисунок 6 - Завершающий этап настройки MongoDB Рисунок 6 - Завершающий этап настройки MongoDB](https://habrastorage.org/getpro/habr/upload_files/35b/3d6/9bc/35b3d69bcb18fc07340e7098295f291f.png)
Приступим непосредственно к разработке сервиса взаимодействия с MongoDB.
Разработка функционала
Файловая структура сервиса
Для начала стоит ознакомиться с получившейся структурой сервиса. Она представлена на рисунке 7.
![Рисунок 7 - Структура проекта oodb-mongo-server Рисунок 7 - Структура проекта oodb-mongo-server](https://habrastorage.org/getpro/habr/upload_files/9f8/af9/666/9f8af96668b810126b93d63c394de49f.png)
Точкой входа в сервис является файл Program.cs
В constants расположены константы API-маршрутов, по которым с этим сервисом можно взаимодействовать по HTTP.
В controllers расположены основные контроллеры, которые реализуют CRUD-операции для каждой отдельной коллекции объектов.
В database находятся папки config и context, которые предназначены для конфигурирования подключения к MongoDB и получению контекста этой базы данных.
В models находятся все модели. Отмечу, что с моделями будет одна интересная особенность - входные данные (в базу данных) и выходные (ответ от сервера) будут отличаться, в силу особенностей коммуникации между главным сервисом и текущим.
В utils располагаются утилиты. В данном сервисе это в основном утилита для работы с рефлексией.
Описание конфигурации MongoDB и её контекста
Файл определяющий конфигурацию MongoDB выглядит следующим образом:
namespace oodb_mongo_server.database.config
{
/// <summary>
/// Класс, реализующий интерфейс конфигурации MongoDB
/// </summary>
public class MongoDbConfig : IMongoDbConfig
{
/// <summary>
/// Конструктор с параметром, принимающий в качестве значения интерфейс конфигурации
/// </summary>
/// <param name="config"></param>
public MongoDbConfig(IConfiguration? config)
{
if (config != null)
{
Database = config["MongoDB:Database"];
Port = int.Parse(config["MongoDB:Port"]);
Host = config["MongoDB:Host"];
User = config["MongoDB:User"];
Password = config["MongoDB:Password"];
}
}
/// <summary>
/// Название базы данных
/// </summary>
public string? Database { get; set; }
/// <summary>
/// Хост
/// </summary>
public string? Host { get; set; }
/// <summary>
/// Порт, по которому происходит подключение к базе данных
/// </summary>
public int? Port { get; set; }
/// <summary>
/// Пользователь
/// </summary>
public string? User { get; set; }
/// <summary>
/// Пароль
/// </summary>
public string? Password { get; set; }
/// <summary>
/// Строка подключения (генерируется исхода из других атрибутов)
/// </summary>
public string? ConnectionString
{
get
{
if (string.IsNullOrEmpty(User) || string.IsNullOrEmpty(Password))
{
return $@"mongodb://{Host}:{Port}";
}
return $@"mongodb://{User}:{Password}@{Host}:{Port}";
}
}
}
}
Данный код достаточно прокомментирован и не требует дополнительных пояснений. В основном, данный файл содержит атрибуты, которые используются при подключении к базе данных.
Контекст, в рамках которого будут реализованы CRUD-операции, выглядит следующим образом:
namespace oodb_mongo_server.database.context
{
/// <summary>
/// Класс, реализующий интерфейс контекста MongoDB
/// </summary>
public class DbContext : IDbContext
{
/// <summary>
/// База данных
/// </summary>
private readonly IMongoDatabase _db;
/// <summary>
/// Конструктор с параметром
/// </summary>
/// <param name="config">Конфигурация</param>
public DbContext(IMongoDbConfig config)
{
// Создание нового клиента с передачей строки подключения
var client = new MongoClient(config.ConnectionString);
// Получение доступа к конкретной базе данных
_db = client.GetDatabase(config.Database);
}
/// <summary>
/// Коллекция объектов AdminList
/// </summary>
public IMongoCollection<AdminModel>? AdminList => _db.GetCollection<AdminModel>("AdminList");
/// <summary>
/// Коллекция объектов DataSourceList
/// </summary>
public IMongoCollection<DataSourceModel>? DataSourceList => _db.GetCollection<DataSourceModel>("DataSourceList");
/// <summary>
/// Коллекция объектов HostList
/// </summary>
public IMongoCollection<HostModel>? HostList => _db.GetCollection<HostModel>("HostList");
/// <summary>
/// Коллекция объектов HostServiceList
/// </summary>
public IMongoCollection<HostServiceModel>? HostServiceList => _db.GetCollection<HostServiceModel>("HostServiceList");
/// <summary>
/// Коллекция объектов MonitorAppList
/// </summary>
public IMongoCollection<MonitorAppModel>? MonitorAppList => _db.GetCollection<MonitorAppModel>("MonitorAppList");
/// <summary>
/// Коллекция объектов ServiceList
/// </summary>
public IMongoCollection<ServiceModel>? ServiceList => _db.GetCollection<ServiceModel>("ServiceList");
}
}
В данном файле определены коллекции объектов (в виде публичных атрибутов класса DbContext), которые будут активно использоваться. Эти атрибуты позволяют напрямую обращаться к конкретным коллекциям в БД, полученным в результате подключения к MongoDB.
Описание точки входа в сервис
В точке входа в сервис определены все основные его настройки, подключение к MongoDB, инициализация контроллеров и запуск сервиса.
/*
* Точка входа в сервис ooodb-mongo-server
* **/
using MongoDB.Driver;
using oodb_mongo_server.database.config;
using oodb_mongo_server.database.context;
using oodb_project.controllers;
// Создание приложения
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Создание экземпляра MongoClient (подключение к БД)
MongoClient client = new MongoClient("mongodb://root:example@localhost:27017");
// Выпод коллекций из базы данных (проверка работы подключения)
using (var cursor = await client.ListDatabasesAsync())
{
var databases = cursor.ToList();
foreach (var database in databases)
{
Console.WriteLine(database);
}
}
// Получение контекста базы данных на основе собранной конфигурации
var context = new DbContext(new MongoDbConfig(null)
{
Database = "oodb",
Host = "localhost",
Password = "example",
User = "root",
Port = 27017
});
// Инициализация маршрутов API сервиса
var initMongoController = new InitMongoController(app, context);
initMongoController.InitRoutes();
// Запуск сервиса
app.Run();
Далее будет рассмотрено устройство контроллеров, которые позволяют реализовать бизнес-логику CRUD-операций.
Устройство контроллеров сервиса
Каждый контроллер является наследником абстрактного класса BaseController, который был создан с целью обобщить выполнение некоторых операций, в числе которых Create, Read, Read All, и Delete (Update не автоматизирована в силу сложности и оригинальности исполнения инструкций при данной операции).
Приведу пример обобщения функции для вывода всех объектов в коллекции:
/// <summary>
/// Метод для получения всех объектов определённого типа
/// </summary>
/// <returns>Результат работы функции (массив документов)</returns>
public IResult GetAll()
{
// Проверка подключения к базе данных
if (_collection == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Поиск всех элементов в коллекции определённого типа (входной тип)
var list = _collection.Find(Builders<IT>.Filter.Empty).ToList();
// Результат представлен в отдельной структуре, т.к. данные нужно преобразовать перед отправкой пользователю
var result = new List<OT>();
foreach (var item in list)
{
// Получение всех полей элемента входного типа (IT)
var fields = ReflectionUtil.getFields(item);
// Процедура замены id входной модели (ObjectId) на id выходной модели (String)
var id = fields[0].ToString();
fields.RemoveAt(0);
if(id != null)
{
fields.Insert(0, id);
}
// Создание нового объекта выходного типа OT с передачей в его конструктор определённых полей
var value = Activator.CreateInstance(typeof(OT), fields.ToArray());
// Если value не равен null, то добавляем его в список результатов
if(value != null)
{
result.Add((OT)value);
}
}
// Возвращаем преобразованный массив результатов
return Results.Json(result.ToArray());
}
catch (Exception e)
{
return Results.Json(new MessageModel(e.Message));
}
}
Полный код абстрактного класса
using oodb_mongo_server.models;
using MongoDB.Driver;
using oodb_mongo_server.models.data;
using oodb_project.models;
using oodb_mongo_server.utils;
using MongoDB.Bson;
using System.Reflection;
/*
* В данном файле определён абстрактный класс BaseController, который
* использует механизм обобщений.
* Для данного класса необходимо указать тип для входных параметров и выходных параметров
* Абстрактный класс определяет методы Get, GetAll и Create
* **/
namespace oodb_mongo_server.controllers
{
/// <summary>
/// Абстрактный класс для контроллеров
/// </summary>
public abstract class BaseController<IT, OT>
where IT : IdModel // Тип для входных параметров
where OT : IdDataModel, new() // Тип для выходных параметров
{
// Коллекция входных параметров
protected IMongoCollection<IT>? _collection;
/// <summary>
/// Конструктор абстрактного класса
/// </summary>
/// <param name="collection">Ссылка на коллекцию</param>
public BaseController(IMongoCollection<IT>? collection)
{
_collection = collection;
}
/// <summary>
/// Метод для создания нового объекта коллекции
/// </summary>
/// <param name="data">Данные объекта коллекции</param>
/// <returns>Созданный объект коллекции</returns>
protected IResult Create(OT data)
{
// Проверка подключения к базе данных
if (_collection == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Получение всех полей выходного объекта
var fields = ReflectionUtil.getDataFields(data);
// Получение информации о всех полях
var fieldsInfo = ReflectionUtil.getDataFieldsInfo(data);
if(fields.Count > fieldsInfo.Count)
{
// Удаляем id в модели
fields.RemoveAt(0);
}
// Создаём объект входного типа
var value = Activator.CreateInstance(typeof(IT), fields.ToArray());
if (value != null)
{
// Добавление объекта в коллекцию
_collection.InsertOne((IT)value);
var fieldsValue = ReflectionUtil.getFields((IT)value);
// Процедура замены id входной модели (ObjectId) на id выходной модели (String)
var id = fieldsValue[0].ToString();
fieldsValue.RemoveAt(0);
if (id != null)
{
fieldsValue.Insert(0, id);
}
var result = Activator.CreateInstance(typeof(OT), fieldsValue.ToArray());
// Возвращаем результат поиска, если при создании объекта не получилось значение null
return Results.Json(result);
}
// Возвращаем ошибку
return Results.Json(new MessageModel("Ошибка: невозможно создать объект для коллекции"));
}
catch (Exception e)
{
return Results.Json(new MessageModel(e.Message));
}
}
/// <summary>
/// Метод для получения всех объектов определённого типа
/// </summary>
/// <returns>Результат работы функции (массив документов)</returns>
public IResult GetAll()
{
// Проверка подключения к базе данных
if (_collection == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Поиск всех элементов в коллекции определённого типа (входной тип)
var list = _collection.Find(Builders<IT>.Filter.Empty).ToList();
// Результат представлен в отдельной структуре, т.к. данные нужно преобразовать перед отправкой пользователю
var result = new List<OT>();
foreach (var item in list)
{
// Получение всех полей элемента входного типа (IT)
var fields = ReflectionUtil.getFields(item);
// Процедура замены id входной модели (ObjectId) на id выходной модели (String)
var id = fields[0].ToString();
fields.RemoveAt(0);
if(id != null)
{
fields.Insert(0, id);
}
// Создание нового объекта выходного типа OT с передачей в его конструктор определённых полей
var value = Activator.CreateInstance(typeof(OT), fields.ToArray());
// Если value не равен null, то добавляем его в список результатов
if(value != null)
{
result.Add((OT)value);
}
}
// Возвращаем преобразованный массив результатов
return Results.Json(result.ToArray());
}
catch (Exception e)
{
return Results.Json(new MessageModel(e.Message));
}
}
/// <summary>
/// Метод для получения конкретного объекта из коллекции
/// </summary>
/// <param name="id">Идентификатор искомого объекта</param>
/// <returns>Результат работы метода (конкретный объект)</returns>
public IResult Get(string id)
{
if (_collection == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Поиск конкретного документа по его идентификатору в коллекции
var data = _collection.Find(document => document.Id == ObjectId.Parse(id)).FirstOrDefault();
// Проверка обнаружения объекта в коллекции (при отсутствии возвращаем ошибку)
if (data == null)
{
return Results.Json(new MessageModel($"Экземпляр объекта {typeof(IT).Name} с Id = {id} не обнаружен в БД"));
}
// Преобразование объекта входного типа в объект выходного типа
var fields = ReflectionUtil.getFields(data);
fields.RemoveAt(0);
if (id != null)
{
fields.Insert(0, id);
}
// Создаём объект выходного типа
var value = Activator.CreateInstance(typeof(OT), fields.ToArray());
if (value != null)
{
// Возвращаем результат поиска, если при создании объекта не получилось значение null
return Results.Json((OT)value);
}
return Results.Json("Internal Server Error");
}
catch (Exception)
{
return Results.Json(new MessageModel($"Экземпляра объекта {typeof(IT).Name} с Id = {id} не обнаружен в БД"));
}
}
/// <summary>
/// Метод для удаления объекта из коллекции
/// </summary>
/// <param name="id">Идентификатор объекта</param>
/// <returns>Удалённый объект коллекции</returns>
public IResult Delete(string id)
{
if (_collection == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
var data = _collection.Find(document => document.Id == ObjectId.Parse(id)).FirstOrDefault();
if (data == null)
{
return Results.Json(new MessageModel($"Экземпляра объекта {typeof(IT).Name} с Id = {id} не обнаружен в БД"));
}
// Удаление объекта из коллекции
_collection.DeleteOne(x => x.Id == data.Id);
var fields = ReflectionUtil.getFields(data);
fields.RemoveAt(0);
if (id != null)
{
fields.Insert(0, id);
}
var value = Activator.CreateInstance(typeof(OT), fields.ToArray());
if (value != null)
{
return Results.Json((OT)value);
}
return Results.Json("Internal Server Error");
}
catch (Exception)
{
return Results.Json(new MessageModel($"Экземпляра объекта {typeof(IT).Name} с Id = {id} не обнаружен в БД"));
}
}
}
}
Данный абстрактный класс позволяет обобщить выполнение некоторых операций в контроллерах, но не всех. Для каждого контроллера может быть использованы либо все методы из абстрактного класса, либо ни одного - всё индивидуально.
Можно заменить, что при получении всех значений атрибутов входного типа (IT) в методе осуществляется удаление Id. Дело в том, что для модели типа IT все модели имеют id с типом ObjectId (необходимый для MongoDB), и для конвертации его в выходные модели (тип OT) необходимо удалить значение первого поля, добавить в него id конвертированный в строку (ObjectId -> ToString()) и создать на основе значений этих атрибутов экземпляр объекта типа OT, через Activator.CreateInstance()
(конструктор будет найден подходящий, т.к. id теперь не типа ObjectId, а типа string). Автоматизация процесса конвертации моделей из одного типа в другой является одной из целей применения механизма рефлексии в абстрактном классе.
Обобщающий класс использует утилиту для рефлексии. Далее я приведу пример функции для получения значения полей типа:
/// <summary>
/// Получение значений всех полей объекта определённого типа (базовый тип - IdModel)
/// </summary>
/// <typeparam name="T">Тип целевого объекта</typeparam>
/// <param name="element">Целевой объект</param>
/// <returns>Список значений полей целевого объекта</returns>
public static List<object> getFields<T>(T element) where T : IdModel
{
List<object> fields = new List<object>();
// Определение флагов, по которым будет осуществляться поиск полей целевого объекта
BindingFlags bindingFlags = BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Instance |
BindingFlags.Static;
// Если element.Id не равен null, то добавляем его значение список fields
if (element.Id != null)
{
fields.Add(element.Id);
}
// Проходим по всем атрибутам экземпляра типа данных T и добавляем значение каждого атрибута в fields
foreach (FieldInfo field in element.GetType().GetFields(bindingFlags))
{
var value = field.GetValue(element);
if(value != null)
{
fields.Add(value);
}
}
return fields;
}
Данный код просто реализует функцию получения всех значений из полей экземпляра объекта типа T, который по своей сути ограничивается абстрактным классом с помощью where: where T : IdModel
Это сделано намерено. Здесь следует уточнить, с какой целью это было сделано.
Все входные и выходные модели - разные, поскольку модели связанные с объектами в коллекциях MongoDB должны иметь обязательный параметр ObjectId, который при конвертации в JSON-формат сгенерирует отдельную JSON-структуру, которая основным сервисом не определена. В данном случае основной сервис (oodb-main-server) определяет какого должны быть формата выходные данные.
namespace oodb_mongo_server.models
{
/// <summary>
/// Абстрактный класс модели входных данных.
/// Используется для обобщения типов входных данных используемых внутри сервиса
/// </summary>
public abstract class IdModel
{
public IdModel()
{
Id = ObjectId.GenerateNewId();
}
public IdModel(ObjectId? id)
{
Id = id;
}
[BsonId]
public ObjectId? Id { get; set; }
}
}
Модель IdModel определяет ObjectId, которое необходимо для каждого объекта в коллекции MongoDB.
Пример наследования от данной модели представлен ниже:
namespace oodb_project.models
{
/// <summary>
/// Модель, характеризующая администратора приложения для мониторинга
/// </summary>
public class AdminModel : IdModel
{
public AdminModel() : base() {}
public AdminModel(ObjectId? id, string? email) : base(id)
{
Email = email;
}
public AdminModel(string? email) : base()
{
Email = email;
}
/// <summary>
/// Почтовый адрес пользователя
/// </summary>
public string? Email { get; set; }
}
}
Модель AdminModel наследуется от IdModel, что позволяет использовать экземпляры данной модели при работе с механизмом рефлексии или абстрактным классом. Все модели используемые для работы с MongoDB наследуются таким образом.
Далее я опишу операцию обновления в контроллере AdminController
/// <summary>
/// Обновление объекта в коллекции Admin
/// </summary>
/// <param name="data">Новые данные для объекта</param>
/// <returns>Данные обновлённого объекта</returns>
public IResult Update(AdminDataModel data)
{
if (_collection == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Поиск конкретного объекта
var admin = _collection.Find(document => document.Id == ObjectId.Parse(data.Id)).FirstOrDefault();
if (admin == null)
{
return Results.Json(new MessageModel($"Экземпляра объекта AdminModel с Id = {data.Id} не обнаружен в БД"));
}
// Создание фильтра для поиска объекта в коллекции
var filter = Builders<AdminModel>.Filter.Eq(s => s.Id, ObjectId.Parse(data.Id));
// Создаём определение для обновления объекта в коллекции
var update = Builders<AdminModel>.Update.Set(s => s.Email, data.Email);
// Обновляем объект в коллекции по определённому фильтру
_collection.UpdateOne(filter, update);
}
catch (Exception e)
{
return Results.Json(new MessageModel(e.Message));
}
return Results.Json(data);
}
Полный код контроллера
using MongoDB.Bson;
using MongoDB.Driver;
using oodb_mongo_server.controllers;
using oodb_mongo_server.database.context;
using oodb_project.models;
/*
* В данном файле представлен класс AdminController,
* который является наследником абстрактного класса BaseController.
* Тип "входной" модели для данных, курсирующих внутри системы является AdminModel
* Тип "выходной" модели для данных, возвращаемых пользователю является AdminDataModel
* В данном классе переопределены методы Update и Delete, т.к. логика данных методов
* может различаться от класса к классу и не подлежит обобщению в рамках текущей системы.
* Также в данном классе переопределён метод Create, т.к. требуется явное указание параметров
* метода при отправки его контроллеру.
* Методы Get, GetAll не переопределены и используются из абстрактного класса.
* **/
namespace oodb_project.controllers
{
/// <summary>
/// Класс контроллера для таблицы Admin
/// </summary>
public class AdminController : BaseController<AdminModel, AdminDataModel>
{
// Контекст базы данных
private DbContext _db;
/// <summary>
/// Конструктор класса
/// </summary>
/// <param name="db">Контекст базы данных</param>
public AdminController(DbContext db) : base(db.AdminList) // Вызываем конструктор абстрактного класса
{
_db = db;
}
/// <summary>
/// Обновление объекта в коллекции Admin
/// </summary>
/// <param name="data">Новые данные для объекта</param>
/// <returns>Данные обновлённого объекта</returns>
public IResult Update(AdminDataModel data)
{
if (_collection == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Поиск конкретного объекта
var admin = _collection.Find(document => document.Id == ObjectId.Parse(data.Id)).FirstOrDefault();
if (admin == null)
{
return Results.Json(new MessageModel($"Экземпляра объекта AdminModel с Id = {data.Id} не обнаружен в БД"));
}
// Создание фильтра для поиска объекта в коллекции
var filter = Builders<AdminModel>.Filter.Eq(s => s.Id, ObjectId.Parse(data.Id));
// Создаём определение для обновления объекта в коллекции
var update = Builders<AdminModel>.Update.Set(s => s.Email, data.Email);
// Обновляем объект в коллекции по определённому фильтру
_collection.UpdateOne(filter, update);
}
catch (Exception e)
{
return Results.Json(new MessageModel(e.Message));
}
return Results.Json(data);
}
/// <summary>
/// Создание объекта для коллекции Admin
/// </summary>
/// <param name="data">Данные объекта</param>
/// <returns>Созданный объект</returns>
public new IResult Create(AdminDataModel data)
{
return base.Create(data);
}
/// <summary>
/// Удаление объекта из коллекции
/// </summary>
/// <param name="id">Идентификатор объекта в коллекции</param>
/// <returns>Удалённый объект</returns>
public new IResult Delete(string id)
{
if ((_collection == null) || (_db.MonitorAppList == null))
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
var data = _collection.Find(document => document.Id == ObjectId.Parse(id)).FirstOrDefault();
if (data == null)
{
return Results.Json(new MessageModel($"Экземпляра объекта {typeof(AdminModel).Name} с Id = {id} не обнаружен в БД"));
}
// Удаление элемента в коллекции
_collection.DeleteOne(x => x.Id == data.Id);
// Каскадное удаление (удаляет все записи в коллекции MonitorApp, которые связаны с текущим объектом Admin)
_db.MonitorAppList.DeleteMany(x => x.Admin!.Id == data.Id);
// Возвращение удалённой модели
return Results.Json(new AdminDataModel(data.Id.ToString()!, data.Email!));
}
catch (Exception e)
{
return Results.Json(new MessageModel(e.Message));
}
}
}
}
В данном контроллере используется абстрактный класс BaseController, который берёт на себя ответственность за работу функций Create, Get и GetAll.
Представленная выше функция Update реализована внутри каждого класса (AdminController в том числе) индивидуально.
Каким образом происходит связка контроллеров с конкретными маршрутами? Пример данного кода представлен ниже, на примере всё того же контроллера AdminController:
/* ----------- */
/* CRUD операции для Host */
/* ----------- */
var hostController = new HostController(_db);
_app.MapPost(ApiUrl.API_SAVE_HOST, hostController.Create);
_app.MapPost(ApiUrl.API_UPDATE_HOST, hostController.Update);
_app.MapPost(ApiUrl.API_DELETE_HOST, hostController.Delete);
_app.MapGet(ApiUrl.API_GET_HOST, hostController.Get);
_app.MapGet(ApiUrl.API_GET_ALL_HOST, hostController.GetAll);
Инициализация находится в файле InitMongoController.cs
Весь представленный выше код находится в отдельном репозитории и при желании читатель может с ним ознакомится перейдя по ссылке на репозиторий данного сервиса.
А на этом рассмотрение сервиса работы с MongoDB заканчивается.
Разработка сервиса для работы с Perst (oodb-perst-server)
Проблема поддержки новых версий
К сожалению Perst не работает с новыми версиями C# и .NET (последняя версия Perst поддерживает .NET Framework 4.8 и меньше). По этой причине нет возможности использовать ASP NET Core.
Одним из достоинств микросервисной архитектуры является то, что можно использовать при разработке каждого отдельного сервиса любые технологии и не исключено, что эти технологии будут устаревшими, что в данном случае и является решением проблемы с совместимостью. И такое решение может быть использовано в других проектах.
При переходе от монолитной архитектуры к микросервисной такое уместно (оставлять какой-то функционал на устаревших технологиях, а другой функционал на новых) или же при решении задач с устаревшими библиотеками (что в данном случае и произошло).
Данный сервис был разработан с использованием .NET Framework 4.8 в виде консольного приложения. Для коммуникации с данным сервисом был использован WebSocketSharp, который позволяет создавать WebSocket-сервер, что и было сделано. Также WebSocketSharp позволяет создавать WebSocket-клиент, для взаимодействия с сервером. В общем, полнодуплексный канал связи получается.
Настройка подключения к ООБД
В первую очередь необходимо самостоятельно загрузить необходимые файлы базы данных Perst на свой локальный компьютер (основной интерес представляет файл DLL, она подключается как зависимость).
Сделать это можно с помощью официального сайта. Там же есть руководство пользователя по настройке и установки данной БД.
Код инициализации базы данных расположен в точке входа в приложение и представлен в рамках следующего метода:
/// <summary>
/// Статический метод инициализации объектно-ориентированной базы данных
/// </summary>
static void initPerstDb()
{
// Создание нового хранилища
_db = StorageFactory.Instance.CreateStorage();
// Открытие файла базы данных для записи
_db.Open("perst.dbs", 100);
// Получение корневого класса
if(_db.Root == null)
{
_db.Root = new PerstRoot(_db);
}
// Связка корневого класса с атрибутом текущего класса
_root = (PerstRoot)_db.Root;
}
Чем является корневой элемент базы данных? Всё просто. Если привести аналогию, то это тот же объект, что выступает в роли контекста для базы данных MongoDB в сервисе oodb-mongo-server. Только в данном случае контекст будет для базы данных Perst.
namespace ConsoleApp1.root
{
/// <summary>
/// Класс, представляющий корневой элемент базы данных
/// </summary>
public class PerstRoot : Persistent
{
/// <summary>
/// Индекс для доступа к коллекции объектов Admin
/// </summary>
public FieldIndex idxAdmin;
/// <summary>
/// Индекс для доступа к коллекции объектов Host
/// </summary>
public FieldIndex idxHost;
/// <summary>
/// Индекс для доступа к коллекции объектов MonitorApp
/// </summary>
public FieldIndex idxMonitorApp;
/// <summary>
/// Индекс для доступа к коллекции объектов DataSource
/// </summary>
public FieldIndex idxDataSource;
/// <summary>
/// Индекс для доступа к коллекции объектов HostService
/// </summary>
public FieldIndex idxHostService;
/// <summary>
/// Индекс для доступа к коллекции объектов Service
/// </summary>
public FieldIndex idxService;
/// <summary>
/// Конструктор с параметром
/// </summary>
/// <param name="db">Хранилище</param>
public PerstRoot(Storage db) : base(db)
{
idxAdmin = db.CreateFieldIndex(typeof(AdminModel), "Id", true);
idxHost = db.CreateFieldIndex(typeof(HostModel), "Id", true);
idxMonitorApp = db.CreateFieldIndex(typeof(MonitorAppModel), "Id", true);
idxDataSource = db.CreateFieldIndex(typeof(DataSourceModel), "Id", true);
idxHostService = db.CreateFieldIndex(typeof(HostServiceModel), "Id", true);
idxService = db.CreateFieldIndex(typeof(ServiceModel), "Id", true);
}
public PerstRoot()
{
}
}
}
В общем-то, дополнительных настроек для подключения к базе данных не требуется. Специализированный файл базы данных создаётся автоматически (файл perst.dbs).
Разработка функционала
Файловая структура проекта
![Рисунок 8 - Структура проекта сервиса oodb-perst-server Рисунок 8 - Структура проекта сервиса oodb-perst-server](https://habrastorage.org/getpro/habr/upload_files/aae/b45/18f/aaeb4518f5e5ab198cd612e61e1dda02.png)
В constants определены константы для связывания путей и конкретных операций (аналогия с API).
В controllers располагаются контроллеры верхнего и низкого уровня.
В data расположен класс, используемый для генерации тестовых данных для ООБД.
В models определены модели, которые активно используются сервисом при работе как с ООБД, так и с выводом данных пользователю
В root расположен класс главного корневого элемента ООБД.
Описание точки входа в сервис
Точка входа в сервис выглядит следующим образом:
/// <summary>
/// Точка входа в консольное приложение
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
// Инициализация БД Perst
initPerstDb();
// Наполнение БД тестовыми значениями
var mockData = new MockData(_db, _root);
mockData.generateData();
// Прослушивание WebSocket-соединений
var wssv = new WebSocketServer("ws://127.0.0.1");
wssv.AddWebSocketService("/admin", initAdminHighController);
wssv.AddWebSocketService("/host", initHostHighController);
wssv.AddWebSocketService("/host-service", initHostServiceHighController);
wssv.AddWebSocketService("/monitor-app", initMonitorAppHighController);
wssv.AddWebSocketService("/data-source", initDataSourceHighController);
wssv.AddWebSocketService("/service", initServiceHighController);
// Начало прослушивания соединений
wssv.Start();
// Остановка работы консольного приложения
Console.ReadKey(true);
// Остановка прослушивания соединений
wssv.Stop();
// Закрытие соединения с базой данных
_db.Close();
}
Полный код точки входа
namespace ConsoleApp1
{
internal class Program
{
/// <summary>
/// Хранилище
/// </summary>
static private Storage _db;
/// <summary>
/// Корневой класс
/// </summary>
static private PerstRoot _root;
/// <summary>
/// Статический метод инициализации объектно-ориентированной базы данных
/// </summary>
static void initPerstDb()
{
// Создание нового хранилища
_db = StorageFactory.Instance.CreateStorage();
// Открытие файла базы данных для записи
_db.Open("perst.dbs", 100);
// Получение корневого класса
if(_db.Root == null)
{
_db.Root = new PerstRoot(_db);
}
// Связка корневого класса с атрибутом текущего класса
_root = (PerstRoot)_db.Root;
}
/// <summary>
/// Инициализация верхнеуровнего контроллера для коллекции объектов Admin
/// </summary>
/// <returns>Экземпляр верхнеуровневого контроллера Admin</returns>
static AdminHighController initAdminHighController()
{
return new AdminHighController(_db, _root);
}
/// <summary>
/// Инициализация верхнеуровнего контроллера для коллекции объектов Host
/// </summary>
/// <returns>Экземпляр верхнеуровневого контроллера Host</returns>
static HostHighController initHostHighController()
{
return new HostHighController(_db, _root);
}
/// <summary>
/// Инициализация верхнеуровнего контроллера для коллекции объектов HostService
/// </summary>
/// <returns>Экземпляр верхнеуровневого контроллера HostService</returns>
static HostServiceHighController initHostServiceHighController()
{
return new HostServiceHighController(_db, _root);
}
/// <summary>
/// Инициализация верхнеуровнего контроллера для коллекции объектов MonitorApp
/// </summary>
/// <returns>Экземпляр верхнеуровневого контроллера MonitorApp</returns>
static MonitorAppHighController initMonitorAppHighController()
{
return new MonitorAppHighController(_db, _root);
}
/// <summary>
/// Инициализация верхнеуровнего контроллера для коллекции объектов DataSource
/// </summary>
/// <returns>Экземпляр верхнеуровневого контроллера DataSource</returns>
static DataSourceHighController initDataSourceHighController()
{
return new DataSourceHighController(_db, _root);
}
/// <summary>
/// Инициализация верхнеуровнего контроллера для коллекции объектов Service
/// </summary>
/// <returns>Экземпляр верхнеуровневого контроллера Service</returns>
static ServiceHighController initServiceHighController()
{
return new ServiceHighController(_db, _root);
}
/// <summary>
/// Точка входа в консольное приложение
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
// Инициализация БД Perst
initPerstDb();
// Наполнение БД тестовыми значениями
var mockData = new MockData(_db, _root);
mockData.generateData();
// Прослушивание WebSocket-соединений
var wssv = new WebSocketServer("ws://127.0.0.1");
wssv.AddWebSocketService("/admin", initAdminHighController);
wssv.AddWebSocketService("/host", initHostHighController);
wssv.AddWebSocketService("/host-service", initHostServiceHighController);
wssv.AddWebSocketService("/monitor-app", initMonitorAppHighController);
wssv.AddWebSocketService("/data-source", initDataSourceHighController);
wssv.AddWebSocketService("/service", initServiceHighController);
// Начало прослушивания соединений
wssv.Start();
// Остановка работы консольного приложения
Console.ReadKey(true);
// Остановка прослушивания соединений
wssv.Stop();
// Закрытие соединения с базой данных
_db.Close();
}
}
}
В точке входа в текущий сервис происходит инициализация подключения к базе данных Perst, наполнение базы данных тестовыми значениями, добавление WebSocket-сервисов к экземпляру WebSocketServer (каждый из которых отвечает за определённую коллекцию объектов), запуск прослушивания соединений и этапы завершения работы сервиса.
Так как взаимодействие с данным сервисом реализовано с помощью WebSocket-соединения, то основной сервис и текущий обмениваются данными в JSON-формате, который заранее определён. Модель, которая используется при конвертации входных данных в текущем сервисе представлена ниже:
namespace ConsoleApp1.models
{
/// <summary>
/// Класс модели, которая используется для взаимодействия с другими сервисами
/// </summary>
public class HttpModel
{
public HttpModel()
{
}
public HttpModel(string path, string payload)
{
Path = path;
Payload = payload;
}
/// <summary>
/// Путь, по которому отправить данные
/// </summary>
public string Path { get; set; }
/// <summary>
/// Отправляемые данные
/// </summary>
public string Payload { get; set; }
}
}
Данная модель определяет полезные данные (Payload) и путь, по которому отправить эти данные (Path, используется для отправки данных конкретной CRUD-операции).
Устройство контроллеров
Контроллеры я разделил на две части:
Верхнеуровневые контроллеры - контроллеры, которые по пути (Path) определяют, какой конкретно CRUD-операции низкого уровня отправлять данные (Payload).
Низкоуровневые контроллеры - контроллеры, которые реализуют конкретные операции с ООБД
Абстрактный класс для верхнеуровневых контроллеров выглядит следующим образом:
namespace ConsoleApp1.controllers.high_level
{
/// <summary>
/// Абстрактный класс верхнеуровневых контроллеров
/// </summary>
public abstract class BaseHighController : WebSocketBehavior
{
// Ссылка на обработчик нижнего уровня
protected IBaseLowController _controller;
public BaseHighController(IBaseLowController controller)
{
_controller = controller;
}
/// <summary>
/// Метод, обрабатывающий приход сообщений
/// </summary>
/// <param name="e"></param>
protected override void OnMessage(MessageEventArgs e)
{
// Вызов функции InitSendRoute и передача ему десериализованных данных, полученных от вызывающей стороны
InitSendRoute(JsonConvert.DeserializeObject<HttpModel>(e.Data));
}
/// <summary>
/// Инициализация вызовов процедур, в зависимости от требуемого пути
/// </summary>
/// <param name="body">Данные, полученные от вызывающей стороны</param>
public void InitSendRoute(HttpModel body)
{
if (body.Path == ApiPerstServiceUrl.GET_ALL)
{
Send(_controller.getAll());
return;
}
else if (body.Path == ApiPerstServiceUrl.GET)
{
Send(_controller.get(body.Payload));
return;
}
else if (body.Path == ApiPerstServiceUrl.CREATE)
{
Send(_controller.create(body.Payload));
return;
}
else if (body.Path == ApiPerstServiceUrl.UPDATE)
{
Send(_controller.update(body.Payload));
return;
}
else if (body.Path == ApiPerstServiceUrl.DELETE)
{
Send(_controller.delete(body.Payload));
return;
}
Send(JsonConvert.SerializeObject(new MessageModel("Not found 404")));
}
}
}
Цель данного класса аналогична той, которая была у предыдущего сервиса.
В данном случае абстрактный класс реализует функцию InitSendRoute()
, в которой идёт распределение Payload операциям расположенным по разным путям.
Переопределённый метод OnMessage()
выступает в роли обработчика события "данным WebSocket-сервисом получено сообщение от WebSocket-клиента". Он просто вызывает InitSendRoute()
с передачей ему десериализованных данных по общей модели.
С данным абстрактным классом определение классов-контроллеров высокого уровня очень небольшое. Например, для контроллера AdminHighController будет следующее определение:
namespace ConsoleApp1
{
/// <summary>
/// Обработчик верхнего уровня для коллекции объектов Admin
/// </summary>
public class AdminHighController : BaseHighController
{
public AdminHighController(Storage db, PerstRoot root) : base(new AdminLowController(db, root)){}
}
}
А что же с низкоуровневыми контроллерами?
Здесь чуть сложнее. Для низкоуровневых контроллеров я использовал другой подход: я определил интерфейс и наследуясь от данного интерфейса реализовал методы контроллеров.
Получилось достаточно много boilerplate-кода, однако для демонстрации работы с интерфейсами оно того стоило. Достижение разнообразия в реализации микросервисов отчасти отражает саму концепцию избранного архитектурного паттерна (любой сервис может быть реализован как угодно и с какими угодно технологиями).
Определение интерфейса низкоуровневых контроллеров
namespace ConsoleApp1.controllers.low_level
{
/// <summary>
/// Интерфейс базового контроллера
/// </summary>
public interface IBaseLowController
{
/// <summary>
/// Обновление объекта
/// </summary>
/// <param name="obj">Новые данные объекта</param>
/// <returns>Данные обновлённого объекта</returns>
string update(string obj);
/// <summary>
/// Создание объкета
/// </summary>
/// <param name="obj">Данные объекта</param>
/// <returns>Данные созданного объекта</returns>
string create(string obj);
/// <summary>
/// Получение списка объектов
/// </summary>
/// <returns>Список объектов</returns>
string getAll();
/// <summary>
/// Получение конкретного объекта
/// </summary>
/// <param name="id">Идентификатор конкретного объекта</param>
/// <returns>Данные объекта</returns>
string get(string id);
/// <summary>
/// Удаление конкретного объекта
/// </summary>
/// <param name="obj">Данные объекта для удаления</param>
/// <returns>Удалённый объект</returns>
string delete(string obj);
}
}
Приведу пример реализации метода удаления в низкоуровневом контроллере HostLowController:
/// <summary>
/// Удаление объекта HostModel
/// </summary>
public string delete(string id)
{
if (_db == null)
{
return JsonConvert.SerializeObject(new MessageModel("Подключение к ООБД отсутствует"));
}
HostOutputModel hostOutput;
try
{
// Поиск модели Host по идентификатору
HostModel host = (HostModel)_root.idxHost[id];
// Проверка на нахождение модели по id
if (host == null)
{
return JsonConvert.SerializeObject(new MessageModel("Объекта по данному ID нет в БД"));
}
// Сборка выходной модели
hostOutput = new HostOutputModel(
host.Id,
host.Name,
host.Url,
host.IPv4,
host.System
);
// Каскадное удаление
host.CascadeDelete(_root);
}
catch (Exception e)
{
return JsonConvert.SerializeObject(new MessageModel(e.Message));
}
return JsonConvert.SerializeObject(hostOutput);
}
Полный код HostLowController.cs
namespace ConsoleApp1.controllers.low_level
{
/// <summary>
/// Класс низкоуровневого контроллера для коллекции объектов Host
/// </summary>
internal class HostLowController : IBaseLowController
{
/// <summary>
/// Хранилище
/// </summary>
private static Storage _db;
/// <summary>
/// Корневой элемент ООБД
/// </summary>
private static PerstRoot _root;
public HostLowController(Storage db, PerstRoot root)
{
_db = db;
_root = root;
}
/// <summary>
/// Обновление объекта HostModel
/// </summary>
public string update(string obj)
{
// Проверка на подключение к ООБД
if (_db == null)
{
return JsonConvert.SerializeObject(new MessageModel("Подключение к ООБД отсутствует"));
}
// Десериализация входных данных
var data = JsonConvert.DeserializeObject<HostOutputModel>(obj);
try
{
// Поиск объекта host в коллекции объектов по ID
HostModel host = (HostModel)_root.idxHost[data.Id];
// Проверка обнаружения объекта
if (host == null)
{
return JsonConvert.SerializeObject(new MessageModel("Объекта по данному ID нет в БД"));
}
// Имзенение данных в найденном объекте host
host.Url = data.Url;
host.Name = data.Name;
host.IPv4 = data.IPv4;
host.System = data.System;
// Фиксация изменений
host.Modify();
}
catch (Exception e)
{
return JsonConvert.SerializeObject(new MessageModel(e.Message));
}
return JsonConvert.SerializeObject(data);
}
/// <summary>
/// Создание объекта HostModel
/// </summary>
public string create(string obj)
{
if (_db == null)
{
return JsonConvert.SerializeObject(new MessageModel("Подключение к ООБД отсутствует"));
}
var data = JsonConvert.DeserializeObject<HostOutputModel>(obj);
try
{
// Добавление уникального идентификатора объекту
data.Id = Guid.NewGuid().ToString();
// Добавление объекта в коллекцию Host
_root.idxHost.Put(new HostModel(data.Id, data.Name, data.Url, data.IPv4, data.System, _db.CreateLink(), _db.CreateLink()));
}
catch (Exception e)
{
return JsonConvert.SerializeObject(new MessageModel(e.Message));
}
return JsonConvert.SerializeObject(data);
}
/// <summary>
/// Получение всех объектов AdminModel
/// </summary>
public string getAll()
{
if (_db == null)
{
return JsonConvert.SerializeObject(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Получение всех данных коллекции
HostOutputModel[] items = new HostOutputModel[_root.idxHost.Count];
// Процесс конвертации полученных данных в выходные данные (аналогично сервису oodb-mongo-server)
for (var i = 0; i < _root.idxHost.Count; i++)
{
HostModel item = (HostModel)_root.idxHost.GetAt(i);
items[i] = new HostOutputModel(
item.Id,
item.Name,
item.Url,
item.IPv4,
item.System
);
}
// Сериализация результата
return JsonConvert.SerializeObject(items);
}
catch (Exception e)
{
return JsonConvert.SerializeObject(new MessageModel(e.Message));
}
}
/// <summary>
/// Получение конкретного объекта HostModel
/// </summary>
public string get(string id)
{
if (_db == null)
{
return JsonConvert.SerializeObject(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
HostOutputModel host = null;
// Поиск объекта в коллекции объектов с конвертацией в выходную модель, при нахождении элемента
for (var i = 0; i < _root.idxHost.Count; i++)
{
HostModel item = (HostModel)_root.idxHost.GetAt(i);
if (item.Id == id)
{
host = new HostOutputModel(
item.Id,
item.Name,
item.Url,
item.IPv4,
item.System
);
break;
}
}
return JsonConvert.SerializeObject(host);
}
catch (Exception e)
{
return JsonConvert.SerializeObject(new MessageModel(e.Message));
}
}
/// <summary>
/// Удаление объекта HostModel
/// </summary>
public string delete(string id)
{
if (_db == null)
{
return JsonConvert.SerializeObject(new MessageModel("Подключение к ООБД отсутствует"));
}
HostOutputModel hostOutput;
try
{
HostModel host = (HostModel)_root.idxHost[id];
if (host == null)
{
return JsonConvert.SerializeObject(new MessageModel("Объекта по данному ID нет в БД"));
}
hostOutput = new HostOutputModel(
host.Id,
host.Name,
host.Url,
host.IPv4,
host.System
);
// Каскадное удаление объекта
host.CascadeDelete(_root);
}
catch (Exception e)
{
return JsonConvert.SerializeObject(new MessageModel(e.Message));
}
return JsonConvert.SerializeObject(hostOutput);
}
}
}
В HostLowController, при удалении объекта реализуется механизм каскадного удаления. На примере данного метода я расскажу о том, каким образом он реализован.
При выполнении host.CascadeDelete(_root)
осуществляется каскадное удаление конкретного объекта host из коллекции Host.
Если перейти к определению модели HostModel, то можно узнать каким образом реализуется механика каскадного удаления.
Механика каскадного удаления выглядит следующим образом:
/// <summary>
/// Каскадное удаление текущего экземпляра объекта из ООБД
/// </summary>
/// <param name="root">Корневой элемент ООБД</param>
/// <returns></returns>
public bool CascadeDelete(PerstRoot root)
{
// Проход по всем элементам коллекции HostServiceLink
foreach (HostServiceModel item in HostServiceLink)
{
// Удаление связанных объектов
item.Delete(root);
}
// Проход по всем элементам коллекции MonitorAppLink
foreach (MonitorAppModel item in MonitorAppLink)
{
// Удаление связанных объектов
item.Delete(root);
}
// Удаление текущего объекта по контексту
return root.idxHost.Remove(this);
}
Полное определение модели HostModel
namespace oodb_project.models
{
/// <summary>
/// Модель, характеризующая конкретный удалённый хост
/// </summary>
public class HostModel : Persistent
{
public HostModel()
{
}
public HostModel(string id, string name, string url, string iPv4, string system, Link hostServiceLink, Link monitorAppLink)
{
Id = id;
Name = name;
Url = url;
IPv4 = iPv4;
System = system;
HostServiceLink = hostServiceLink;
MonitorAppLink = monitorAppLink;
}
/// <summary>
/// Идентификатор объекта
/// </summary>
public string Id { get; set; }
/// <summary>
/// Имя
/// </summary>
public string Name { get; set; }
/// <summary>
/// Адрес
/// </summary>
public string Url { get; set; }
/// <summary>
/// IP
/// </summary>
public string IPv4 { get; set; }
/// <summary>
/// Название системы
/// </summary>
public string System { get; set; }
/// <summary>
/// Ссылка на объект HostService (который ссылается на данный объект, в реляционном представлении)
/// </summary>
public Link HostServiceLink { get; set; }
/// <summary>
/// Ссылка на объект MonitorApp (который ссылается на данный объект, в реляционном представлении)
/// </summary>
public Link MonitorAppLink { get; set; }
/// <summary>
/// Каскадное удаление текущего экземпляра объекта из ООБД
/// </summary>
/// <param name="root">Корневой элемент ООБД</param>
/// <returns></returns>
public bool CascadeDelete(PerstRoot root)
{
// Проход по всем элементам коллекции HostServiceLink
foreach (HostServiceModel item in HostServiceLink)
{
// Удаление связанных объектов
item.Delete(root);
}
// Проход по всем элементам коллекции MonitorAppLink
foreach (MonitorAppModel item in MonitorAppLink)
{
// Удаление связанных объектов
item.Delete(root);
}
// Удаление текущего объекта по контексту
return root.idxHost.Remove(this);
}
}
}
При каскадном удалении осуществляется проход по всем элементам связанных коллекций и удаление конкретных элементов в этих коллекциях. И только после удаления всех связанных объектов осуществляется удаление текущего.
Каждая модель, которая используется для работы с базой данных Perst наследуется от Persistent
Вот пример:
/// <summary>
/// Модель, характеризующая источник данных для сервиса
/// </summary>
public class DataSourceModel : Persistent
И это наследование очень мешает при преобразовании объекта в строку (сериализации). Поэтому было принято решение также как и в сервисе oodb-mongo-server производить деление моделей которые используются внутри системы (для работы с ООБД) и моделей, которые возвращаются основному сервису.
Таким образом реализован сервис oodb-perst-server.
Для более детального ознакомления с исходным кодом приложения следует перейти по ссылке на репозиторий сервиса.
Разработка сервиса для работы с db4o (oodb-main-server)
Файловая структура сервиса
![Рисунок 9 - Файловая структура сервиса Рисунок 9 - Файловая структура сервиса](https://habrastorage.org/getpro/habr/upload_files/78c/253/58a/78c25358a5e42077d164d2c0ffe98e9c.png)
В constants расположены константы, связывающий все маршруты API с контроллерами.
В controllers определены три множества контроллеров, каждый из которых реализует взаимодействие с тем или иным сервисом. В рамках данной статьи будут рассмотрены только контроллеры для db4o, для ознакомления с работой других контроллеров я предлагаю читателю обратиться к репозиторию исходного кода.
В data расположен класс, реализующий логику наполнения db4o тестовыми данными.
В models располагаются модели, активно используемые в данном сервисе.
Настройка подключения к базе данных
Подключить db4o можно достаточно просто - для этого можно воспользоваться пакетным менеджером NuGet и просто загрузить все необходимые компоненты для работы с данной библиотекой.
![Рисунок 10 - Установленная библиотека db4o-devel Рисунок 10 - Установленная библиотека db4o-devel](https://habrastorage.org/getpro/habr/upload_files/413/0cb/b81/4130cbb8180c8f692db0b9d514913e8e.png)
Код в точке входа, который соответствует настройке и конфигурированию db4o выглядит следующим образом:
// При запуске проверяем есть ли файл БД, и если есть - удаляем его
if (File.Exists("db4o.yap"))
{
File.Delete("db4o.yap");
}
// Конфигурирование ООБД (db4o)
IObjectContainer dbDb4o = Db4oEmbedded.OpenFile(Db4oEmbedded.NewConfiguration(), "db4o.yap");
// Заполнение ООБД тестовыми данными
var mockData = new MockData(dbDb4o);
mockData.generateData();
Разработка функционала
Описание точки входа в сервис
Код точки входа выглядит следующим образом:
/*
* Точка входа в сервис oodb-main-server
* **/
using Db4objects.Db4o;
using oodb_project.controllers.db4o;
using oodb_project.controllers.mongo;
using oodb_project.controllers.perst;
using oodb_project.data;
// Создание экземпляра класса WebApplicationBuilder для конфигурирования веб-приложения
var builder = WebApplication.CreateBuilder(args);
// Сборка веб-приложения
var app = builder.Build();
// Конфигурирование статических путей к файлам
app.UseDefaultFiles();
app.UseStaticFiles();
// При запуске проверяем есть ли файл БД, и если есть - удаляем его
if (File.Exists("db4o.yap"))
{
File.Delete("db4o.yap");
}
// Конфигурирование ООБД (db4o)
IObjectContainer dbDb4o = Db4oEmbedded.OpenFile(Db4oEmbedded.NewConfiguration(), "db4o.yap");
// Заполнение ООБД тестовыми данными
var mockData = new MockData(dbDb4o);
mockData.generateData();
// Инициализация маршрутов для работы с ООБД db4o
var initDb4oController = new InitDb4oController(app, dbDb4o);
initDb4oController.InitRoutes();
// Инициализация маршрутов для работы с ООДБ perst
var initPerstController = new InitPerstController(app);
initPerstController.InitRoutes();
// Инициализация маршрутов для работы с MongoDB
var initMongoController = new InitMongoController(app);
initMongoController.InitRoutes();
// Запуск серверного приложения
app.Run();
// Закрытие соединений с базой данных
dbDb4o.Close();
В точке входа осуществляется сборка веб-приложения, настройка и конфигурирование ООБД db4o, инициализация маршрутов для работы с db4o, MongoDB и Perst и запуск серверного приложения. Все соответствующие этапы приведены к комментариях к коду.
Устройство контроллеров
В данном сервисе контроллеры устроены аналогично сервису oodb-mongo-server - также каждый контроллер наследуется от абстрактного базового класса, который покрывает определённое множество операций. Однако в данном случае обошлось без механизма рефлексии.
Например, так выглядит функция создания нового объекта:
/// <summary>
/// Метод для создания нового объекта коллекции
/// </summary>
/// <param name="data">Данные объекта коллекции</param>
/// <returns>Созданный объект коллекции</returns>
protected IResult Create(T data)
{
// Проверка подключения к базе данных
if (_db == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Автоматическая генерация UUID
data.Id = Guid.NewGuid().ToString();
// Сохранение модели в ООДБ
_db.Store(data);
}
catch (Exception e)
{
return Results.Json(new MessageModel(e.Message));
}
return Results.Json(data);
}
Полный код абстрактного класса контроллеров db4o
namespace oodb_project.controllers.db4o
{
/// <summary>
/// Абстрактный класс, реализующий обобщённые методы CRUD-операций для коллекций объектов
/// </summary>
/// <typeparam name="T">Тип данных моделей, используемых в рамках CRUD-операций</typeparam>
public abstract class BaseController<T>
where T : IdModel
{
// Коллекция входных параметров
protected IObjectContainer? _db;
/// <summary>
/// Конструктор абстрактного класса
/// </summary>
/// <param name="collection">Ссылка на коллекцию</param>
public BaseController(IObjectContainer? db)
{
_db = db;
}
/// <summary>
/// Метод для создания нового объекта коллекции
/// </summary>
/// <param name="data">Данные объекта коллекции</param>
/// <returns>Созданный объект коллекции</returns>
protected IResult Create(T data)
{
// Проверка подключения к базе данных
if (_db == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Автоматическая генерация UUID
data.Id = Guid.NewGuid().ToString();
// Сохранение модели в ООДБ
_db.Store(data);
}
catch (Exception e)
{
return Results.Json(new MessageModel(e.Message));
}
return Results.Json(data);
}
/// <summary>
/// Метод для получения всех объектов определённого типа
/// </summary>
/// <returns>Результат работы функции (массив документов)</returns>
public IResult GetAll()
{
if (_db == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Поиск всех данных по текущей модели
IObjectSet result = _db.QueryByExample(typeof(T));
// Возвращение списка моделей
return Results.Json(result);
}
catch (Exception e)
{
return Results.Json(new MessageModel(e.Message));
}
}
/// <summary>
/// Метод для получения конкретного объекта из коллекции
/// </summary>
/// <param name="id">Идентификатор искомого объекта</param>
/// <returns>Результат работы метода (конкретный объект)</returns>
public IResult Get(string id)
{
if (_db == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Получение конкретной модели
T data = _db.Query<T>(value => value.Id == id)[0];
return Results.Json(data);
}
catch (Exception)
{
return Results.Json(new MessageModel($"Модели с Id = {id} нет в ООБД"));
}
}
/// <summary>
/// Метод для удаления объекта из коллекции
/// </summary>
/// <param name="id">Идентификатор объекта</param>
/// <returns>Удалённый объект коллекции</returns>
public IResult Delete(string id)
{
if (_db == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Получение конкретного объекта
var data = _db.Query<T>(value => value.Id == id);
if(data.Count <= 0)
{
return Results.Json(new MessageModel($"Модели с Id = {id} нет в ООБД"));
}
var cloneData = data.First();
// Удаление объекта
_db.Delete(data.First());
return Results.Json(cloneData);
}
catch (Exception)
{
return Results.Json(new MessageModel($"Модели с Id = {id} нет в ООБД"));
}
}
}
}
При создании нового объекта на вход поступает объект типа T, который определяется классом через where: where T : IdModel
, что позволяет обобщить CRUD-операции, ведь все модели, которые в них участвуют, наследуются от абстрактного класса IdModel, представляющую идентификатор модели.
Приведу пример реализации операции обновления из контроллера DataSourceController:
/// <summary>
/// Обновление объекта в коллекции
/// </summary>
/// <param name="data">Данные об объекте в коллекции</param>
/// <returns>Обновлённый объект</returns>
public IResult Update(DataSourceModel data)
{
// Проверка подключения к базе данных
if (_db == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Поиск объекта по идентификатору
DataSourceModel findObj = _db.Query<DataSourceModel>(value => value.Id == data.Id)[0];
// Изменение данных в объекте
findObj.Url = data.Url;
findObj.Name = data.Name;
// Фиксация изменений
_db.Store(findObj);
}
catch (Exception e)
{
return Results.Json(new MessageModel(e.Message));
}
return Results.Json(data);
}
Полный код контроллера DataSourceController
namespace oodb_project.controllers.db4o
{
/// <summary>
/// Класс определяющий контроллеры для коллекции объектов DataSource
/// </summary>
public class DataSourceController : BaseController<DataSourceModel>
{
public DataSourceController(IObjectContainer db) : base(db) { }
/// <summary>
/// Обновление объекта в коллекции
/// </summary>
/// <param name="data">Данные об объекте в коллекции</param>
/// <returns>Обновлённый объект</returns>
public IResult Update(DataSourceModel data)
{
if (_db == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
DataSourceModel findObj = _db.Query<DataSourceModel>(value => value.Id == data.Id)[0];
findObj.Url = data.Url;
findObj.Name = data.Name;
_db.Store(findObj);
}
catch (Exception e)
{
return Results.Json(new MessageModel(e.Message));
}
return Results.Json(data);
}
/// <summary>
/// Создание нового объекта коллекции
/// </summary>
/// <param name="data">Данные об объекте</param>
/// <returns>Созданный объект</returns>
public new IResult Create(DataSourceModel data)
{
return base.Create(data);
}
/// <summary>
/// Получение всех объектов коллекции с помощью SODA-запроса
/// </summary>
/// <returns>Список всех объектов коллекции</returns>
public new IResult GetAll()
{
if (_db == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
// Получение всех записей из DataSource с помощью SODA-запроса
IQuery query = _db.Query();
// Установка ограничений для поиска
query.Constrain(typeof(DataSourceModel));
// Получение результата поиска
IObjectSet result = query.Execute();
return Results.Json(result);
}
catch (Exception e)
{
return Results.Json(new MessageModel(e.Message));
}
}
/// <summary>
/// Каскадное удаление объекта DataSource
/// </summary>
/// <param name="id">Идентификатор объекта в коллекции</param>
/// <returns>Удалённый объект</returns>
public new IResult Delete(string id)
{
if (_db == null)
{
return Results.Json(new MessageModel("Подключение к ООБД отсутствует"));
}
try
{
var data = _db.Query<DataSourceModel>(value => value.Id == id);
if (data.Count <= 0)
{
return Results.Json(new MessageModel($"Модели с Id = {id} нет в ООБД"));
}
var services = _db.Query<ServiceModel>(value => value.DataSourceId == id);
foreach (var item in services)
{
var hostServices = _db.Query<HostServiceModel>(value => value.ServiceId == item.Id);
foreach(var hostService in hostServices)
{
_db.Delete(hostService);
}
_db.Delete(item);
}
var cloneData = data.First();
// Удаление модели
_db.Delete(data.First());
return Results.Json(cloneData);
}
catch (Exception)
{
return Results.Json(new MessageModel($"Модели с Id = {id} нет в ООБД"));
}
}
}
}
Как видно из примера, сначала происходит поиск объекта в коллекции по идентификатору, затем происходит изменение его полей и фиксация изменений через объект базы данных.
Разработка desktop-клиента (oodb-desktop-client)
В заключительной части статьи будут описаны результаты разработки desktop-клиента, который отправляет запросы на сервис oodb-mongo-server, через основной сервис oodb-main-server. То есть, клиент работает с базой данных MongoDB.
Файловая структура проекта
![Рисунок 11 - Файловая структура oodb-desktop-client Рисунок 11 - Файловая структура oodb-desktop-client](https://habrastorage.org/getpro/habr/upload_files/65f/107/6e4/65f1076e405d98dedfe7eca66b1d10dd.png)
В constants расположены все важные константы, которые используются в клиентском приложении.
В generators располагаются классы, определяющие методы, в которых происходит генерация объектов для конкретных коллекций.
В models расположены модели.
В services определены сервисы, с помощью которых реализуется логика взаимодействия с основным сервером oodb-main-server.
В utils находятся утилиты
Общий вид клиентского приложения
![Рисунок 12 - Визуальное представление клиентского приложения Рисунок 12 - Визуальное представление клиентского приложения](https://habrastorage.org/getpro/habr/upload_files/964/10f/603/96410f6034495f75b8a08d3a4940b692.png)
Приложение снабжено различными элементами управления, которые позволяют производить операции с конкретными коллекциями. На рисунке 12 представлено взаимодействие с коллекцией Host.
Разработка функционала
Устройство сервисов
Разберём устройство сервисов на примере AdminService.
namespace oodb_desktop_client.services
{
/// <summary>
/// Класс сервиса для коллекции объектов Admin
/// </summary>
public class AdminService : BaseService
{
public AdminService(DataGridView data) : base(data) {}
/// <summary>
/// Получение всех записей из таблицы Admins
/// </summary>
public void GetAll()
{
GetAll<AdminModel>(ApiUrl.ADMIN, "AdminId1");
}
/// <summary>
/// Сохранение новой записи в таблицу Admins
/// </summary>
public void Save(AdminModel body)
{
Save(body, ApiUrl.ADMIN);
}
/// <summary>
/// Обновление записи в таблице Admins
/// </summary>
/// <param name="body">Новые данные</param>
public void Update(AdminModel body)
{
Update(body, ApiUrl.ADMIN, "AdminId1");
}
/// <summary>
/// Удаление записи из таблицы Admins
/// </summary>
/// <param name="id">ID записи в ООБД</param>
public void Delete(string id)
{
Delete<AdminModel>(id, ApiUrl.ADMIN, "AdminId1");
}
/// <summary>
/// Формирование новой записи
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public void ShowDialog(
string text, // Текст
string operation = "save", // Операция (save / update)
AdminModel oldValue = null // Старое значение (опционально)
)
{
// Создание экземпляра формы
Form prompt = new Form()
{
Width = 300, // Ширина
Height = 200, // Высота
FormBorderStyle = FormBorderStyle.FixedDialog, // Стиль границы формы
Text = text, // Текст формы
StartPosition = FormStartPosition.CenterScreen // Стартовая позиция
};
// Текст перед строкой ввода
Label textLabel = new Label() { Left = 100, Top = 20, Width = 100, Text = "Введите Email" };
// Строка ввода данных
TextBox textBox = new TextBox() { Left = 100, Top = 50, Width = 100, Text = (oldValue != null)? oldValue.Email : "" };
// Кнопка отправки данных
Button confirmation = new Button() { Text = text, Left = 100, Width = 100, Top = 90, DialogResult = DialogResult.OK };
// Обработчик нажания на кнопку отправки
confirmation.Click += (sender, e) => {
prompt.Close();
};
// Добавление элементов управления на форму
prompt.Controls.Add(textBox);
prompt.Controls.Add(confirmation);
prompt.Controls.Add(textLabel);
prompt.AcceptButton = confirmation;
if(prompt.ShowDialog() != DialogResult.OK)
{
return;
}
// Проверка валидности почтового адреса
if (!ValidateUtil.IsValidEmail(textBox.Text))
{
MessageBox.Show(
"Не правильный формат Email-адреса",
"Ошибка",
MessageBoxButtons.OK,
MessageBoxIcon.Error
);
return;
}
// В зависимости от операции вызываем ту или иную функцию, с экземпляром модели
if(operation == "save")
{
Save(new AdminModel(Guid.NewGuid().ToString(), textBox.Text));
}else if(operation == "update")
{
Update(new AdminModel(oldValue.Id, textBox.Text));
}
}
}
}
Первое, на что можно обратить внимание это наследование от класса BaseService. Данный класс реализует все CRUD-операции, основываясь на механизме рефлексии. С его помощью код сервисов значительно упрощается, однако необходимо определять метод ShowDialog()
, который вызывается при изменении данных в объекте или создании нового объекта для коллекции (create или update), вызывая при этом форму. На рисунке 13 представлен такой вызов, однако для объекта коллекции Host.
![Рисунок 13 - Вызываемая форма при изменении данных объекта в коллекции Рисунок 13 - Вызываемая форма при изменении данных объекта в коллекции](https://habrastorage.org/getpro/habr/upload_files/80f/652/b09/80f652b09680bcf919e578ae5aa10f9d.png)
Полное определение абстрактного класса BaseService
namespace oodb_desktop_client.services
{
/// <summary>
/// Абстрактный класс определяющий базовые CRUD-операции для работы с таблицами
/// </summary>
public abstract class BaseService
{
public DataGridView dataGridView;
public BaseService(DataGridView data)
{
dataGridView = data;
}
/// <summary>
/// Получение всех данных из таблицы
/// </summary>
/// <typeparam name="T">Тип данных</typeparam>
/// <param name="domainPath">Путь к домену таблицы</param>
/// <param name="columnName">Название столбца первичного ключа</param>
public void GetAll<T>(string domainPath, string columnName) where T : IdModel
{
var url = $"{ApiUrl.BASE_URL}{ApiUrl.MONGO_DOMAIN}{domainPath}{ApiUrl.GET_ALL}";
var httpRequest = (HttpWebRequest)WebRequest.Create(url);
var httpResponse = (HttpWebResponse)httpRequest.GetResponse();
using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
{
var result = streamReader.ReadToEnd();
var list = JsonConvert.DeserializeObject<List<T>>(result.ToString());
foreach (T item in list)
{
if (GridViewUtil.GetIndexByValue(dataGridView, columnName, item.Id) < 0)
{
var fields = ReflectionUtil.getFields(item);
Action action = () => dataGridView.Rows.Add(fields.ToArray());
dataGridView.Invoke(action);
}
}
}
httpResponse.Close();
}
/// <summary>
/// Изменение данных
/// </summary>
/// <param name="body">Данные для изменения</param>
public void Update<T>(T body, string domainPath, string columnName) where T : IdModel
{
var url = $"{ApiUrl.BASE_URL}{ApiUrl.MONGO_DOMAIN}{domainPath}{ApiUrl.UPDATE}";
var httpRequest = (HttpWebRequest)WebRequest.Create(url);
httpRequest.Method = "POST";
httpRequest.ContentType = "application/json";
var data = JsonConvert.SerializeObject(body);
using (var streamWriter = new StreamWriter(httpRequest.GetRequestStream()))
{
streamWriter.Write(data);
}
var httpResponse = (HttpWebResponse)httpRequest.GetResponse();
object output = null;
using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
{
var result = streamReader.ReadToEnd();
output = (object)JsonConvert.DeserializeObject<T>(result.ToString());
if (((T)output).Id == null)
{
MessageBox.Show(
(JsonConvert.DeserializeObject<MessageModel>(result.ToString())).message,
"Ошибка",
MessageBoxButtons.OK,
MessageBoxIcon.Error
);
}
else
{
T model = ((T)output);
var index = GridViewUtil.GetIndexByValue(dataGridView, columnName, model.Id);
var fields = ReflectionUtil.getFields(model);
Action action = () =>
{
for (var i = 0; i < fields.Count; i++)
{
dataGridView.Rows[index].Cells[i].Value = fields[i];
}
};
dataGridView.Invoke(action);
}
}
httpResponse.Close();
}
/// <summary>
/// Сохранение данных
/// </summary>
/// <param name="body">Данные для сохранения</param>
public void Save<T>(T body, string domainPath) where T : IdModel
{
var url = $"{ApiUrl.BASE_URL}{ApiUrl.MONGO_DOMAIN}{domainPath}{ApiUrl.SAVE}";
var httpRequest = (HttpWebRequest)WebRequest.Create(url);
httpRequest.Method = "POST";
httpRequest.ContentType = "application/json";
var data = JsonConvert.SerializeObject(body);
using (var streamWriter = new StreamWriter(httpRequest.GetRequestStream()))
{
streamWriter.Write(data);
}
var httpResponse = (HttpWebResponse)httpRequest.GetResponse();
object output = null;
using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
{
var result = streamReader.ReadToEnd();
output = (object)JsonConvert.DeserializeObject<T>(result.ToString());
if (((T)output).Id == null)
{
MessageBox.Show(
(JsonConvert.DeserializeObject<MessageModel>(result.ToString())).message,
"Ошибка",
MessageBoxButtons.OK,
MessageBoxIcon.Error
);
}
else
{
var fields = ReflectionUtil.getFields((T)output);
Action action = () => dataGridView.Rows.Add(fields.ToArray());
dataGridView.Invoke(action);
}
}
httpResponse.Close();
}
/// <summary>
/// Удаление записи
/// </summary>
/// <param name="admin">Данные для сохранения</param>
public void Delete<T>(string id, string domainPath, string columnId) where T : IdModel
{
var url = $"{ApiUrl.BASE_URL}{ApiUrl.MONGO_DOMAIN}{domainPath}{ApiUrl.DELETE}/{id}";
var httpRequest = (HttpWebRequest)WebRequest.Create(url);
httpRequest.Method = "POST";
httpRequest.ContentType = "application/json";
var httpResponse = (HttpWebResponse)httpRequest.GetResponse();
object output = null;
using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
{
var result = streamReader.ReadToEnd();
output = (object)JsonConvert.DeserializeObject<T>(result.ToString());
if (((T)output).Id == null)
{
MessageBox.Show(
(JsonConvert.DeserializeObject<MessageModel>(result.ToString())).message,
"Ошибка",
MessageBoxButtons.OK,
MessageBoxIcon.Error
);
}
else
{
var index = GridViewUtil.GetIndexByValue(dataGridView, columnId, id);
Action action = () => dataGridView.Rows.RemoveAt(index);
dataGridView.Invoke(action);
}
}
httpResponse.Close();
}
}
}
Генерация данных с использованием Bogus
Для генерации данных из клиента была использована библиотека Bogus, которую можно установить с помощью пакетного менеджера NuGet.
Использование Bogus в рамках текущего приложения будет представлено на примере генератора AdminGenerator:
namespace oodb_desktop_client.generators
{
/// <summary>
/// Класс генератора объектов коллекции Admin
/// </summary>
public class AdminGenerator
{
/// <summary>
/// Сервис Admin
/// </summary>
private AdminService _adminService;
public AdminGenerator(AdminService adminService)
{
_adminService = adminService;
}
/// <summary>
/// Генерация администраторов
/// </summary>
/// <param name="count">Количество генерируемых записей</param>
/// <param name="progress">Прогресс генерации</param>
public void GenerateAdmin(
int count, // Количество сгенерированных элементов
IProgress<ProgressInfo> progress // Прогресс генерации
)
{
// Создание экземпляра класса Faker
var faker = new Faker("ru");
// Процесс генерации объектов
for(var i = 0; i < count; i++)
{
// Получение рандомного почтового адреса
var email = faker.Internet.Email();
// Сохранение объекта в ООБД
_adminService.Save(new AdminModel("", email), ApiUrl.ADMIN);
// Обновление текстовой информации
var info = $"Сгенерировано записей: {i}";
// Отправка изменений о прогрессе генерации записей
progress?.Report(new ProgressInfo
{
value = i + 1,
info = info
});
// Пауза
Thread.Sleep(1);
}
}
}
}
Генерация определённого количества записей происходит в методе GenerateAdmin. В данном методе используется экземпляр объекта Faker из Bogus, который генерирует случайный почтовый адрес пользователя, а затем через сервис AdminService происходит сохранение сгенерированной модели в базу данных.
Ссылка на репозиторий проекта.
Выводы
В данной статье были рассмотрены результаты разработки системы, состоящей из множества сервисов, каждый из которых работает со своей базой данных.
Была спроектирована модель базы данных по описанию предметной области в реляционном представлении, она же и служит ориентиром в обозначении отношений между коллекциями объектов.
Спроектированная архитектура системы полностью соответствует полученным результатам.
Два сервиса осуществляют работу с объектно-ориентированными базами данных Perst и db4o, один сервис взаимодействует с MongoDB, которая была развёрнута в контейнере вместе с веб-интерфейсом Mongo Express.
Клиентское приложение было разработано с помощью .NET Framework 4.8. Оно взаимодействует с основным сервисом (oodb-main-server) и посредством этого взаимодействия получает доступ к сервису oodb-mongo-server.
Было также дано описание проблеме с поддержкой Perst новыми версиями .NET, и описание решения данной проблемы в контексте избранного архитектурного паттерна (микросервисная архитектура).
Список использованных источников
Репозиторий основного сервиса: oodb-main-server
Репозиторий сервиса для взаимодействия с Perst: oodb-perst-server
Репозиторий сервиса для взаимодействия с MongoDB: oodb-mongo-server
Репозиторий клиентского приложения: oodb-desktop-client