В настоящее время объектно-ориентированные базы данных (ООБД) не имеют достаточно большого распространения в повседневном использовании, да и более того, не настолько популярны как реляционные базы данных, которые не один десяток лет уже активно поддерживаются различными сообществами и имеют долгую историю применения.
В данной статье рассматривается реализация ООБД в контексте разработки системы, состоящей из микросервисов, на примере Perst и Db4o. Также будет рассмотрена отдельная реализация с документно-ориентированной базой данных MongoDB, работа с которой имеет много общего с ООБД.
Целью данной статьи является рассмотрение практического применения ООБД и решения проблем совместимости с помощью микросервисной архитектуры.
Введение
При написании данной статьи я руководствовался личным опытом разработки микросервисов и работы с ООБД на языке 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.
В модели базы данных обозначены атрибуты для каждой сущности, с которыми можно ознакомиться в таблицах.
Дополнительно схему базы данных сопровождают отношения между сущностями, количество которых достаточно для осуществления разнопланового тестирования системы: от реализации простых CRUD-операций, до реализации каскадного удаления.
Проектирование архитектуры системы
Что такое микросервисная архитектура? Микросервисная архитектура - это стиль проектирования, который разбивает систему на отдельные сервисы с разными функциями.
Единицей модульности в данном стиле проектирования выступает сервис.
Каждый сервис имеет свою базу данных, в которых находятся все важные данные, используемые при выполнении бизнес-логики.
В случае данной системы сервисы обладают разными базами данных, которые воспроизводят одну и ту же реляционную схему. Всего в системе три сервиса.
Схема спроектированной архитектуры представлена на рисунке 2.
Из рисунка 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
.
При запуске всех контейнеров они должны отображаться в списке контейнеров. Для этого удобно использовать команду docker container ps -a
После того, как контейнеры были развёрнуты следует проверить подключение к MongoDB с помощью веб-интерфейса. В случае текущих настроек, которые были определены в docker-compose, путь к веб-интерфейсу определён по адресу http://localhost:8081
При работе с веб-интерфейсом необходимо создать базу данных oodb (я это уже сделал), и в данной базе данных создать следующие коллекции: AdminList, DataSourceList, HostList, HostServiceList, MonitorAppList, ServiceList.
И при такой настройке MongoDB исходный код, который будет представлен в конце статьи, будет работать со средой тех читателей, которые решат попробовать запустить исходный код.
Приступим непосредственно к разработке сервиса взаимодействия с MongoDB.
Разработка функционала
Файловая структура сервиса
Для начала стоит ознакомиться с получившейся структурой сервиса. Она представлена на рисунке 7.
Точкой входа в сервис является файл 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).
Разработка функционала
Файловая структура проекта
В 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)
Файловая структура сервиса
В constants расположены константы, связывающий все маршруты API с контроллерами.
В controllers определены три множества контроллеров, каждый из которых реализует взаимодействие с тем или иным сервисом. В рамках данной статьи будут рассмотрены только контроллеры для db4o, для ознакомления с работой других контроллеров я предлагаю читателю обратиться к репозиторию исходного кода.
В data расположен класс, реализующий логику наполнения db4o тестовыми данными.
В models располагаются модели, активно используемые в данном сервисе.
Настройка подключения к базе данных
Подключить db4o можно достаточно просто - для этого можно воспользоваться пакетным менеджером NuGet и просто загрузить все необходимые компоненты для работы с данной библиотекой.
Код в точке входа, который соответствует настройке и конфигурированию 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.
Файловая структура проекта
В constants расположены все важные константы, которые используются в клиентском приложении.
В generators располагаются классы, определяющие методы, в которых происходит генерация объектов для конкретных коллекций.
В models расположены модели.
В services определены сервисы, с помощью которых реализуется логика взаимодействия с основным сервером oodb-main-server.
В utils находятся утилиты
Общий вид клиентского приложения
Приложение снабжено различными элементами управления, которые позволяют производить операции с конкретными коллекциями. На рисунке 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.
Полное определение абстрактного класса 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
Lukerman
Тестовое?
Подскажите пожалуйста, может я не внимательно прочитал статью и поэтому не понял для чего вы пишете блокирующий код ?
P.S. За материал + ,аккуратность , оформление.
dan_sw Автор
Доброе утро.
Да, здесь используется механизм блокирующего кода. Устройство контроллеров организованно таким образом, что они в большинстве своём используют в качестве возвращаемого типа IResult, а не Task<IResult>, что было бы уместно при реализации механизма неблокирующего кода.
Такое "упрощение" механизма контроллеров было сделано с допущением, что данный проект по большей части демонстрационный, однако при использовании исходников данного проекта конечно стоит учитывать дополнение всех контроллеров возвращаемым типом Task<IResult>.
В дополнение, могу сказать что такие ООДБ как Perst и db4o не поддерживают асинхронных операций и все операции происходят в одном потоке, что является одним из недостатков избранных ООБД. Однако для работы с MongoDB асинхронные операции предусмотрены, но они не были использованы в данной работе. Одной из причин для этого было обобщение способа взаимодействия с базами данных. В статье я также привёл аналогию между корневым объектом Perst и контекстом MongoDB.