Библиотека для работы с базами данных. Включает в себя абстракции для подключения к БД, выполнения запросов, управления транзакциями и миграциями.
Библиотека состоит из двух Nuget пакетов: Itmo.Dev.Platform.Persistence.Abstractions с абстракциями и
Itmo.Dev.Platform.Persistence.Postgres с реализациями для БД Postgres.
Для регистрации в DI-контейнере необходимо вызвать метод AddPlatformPersistence, предварительно зарегистрировав саму
платформу (метод AddPlatform, в нём необходимо выбрать сериализатор).
Метод принимает делегат, в котором происходит конфигурация Persistence слоя.
collection.AddPlatformPersistence(persistence => persistence.UsePostgres(postgres => postgres
.WithConnectionOptions(static builder => builder
.BindConfiguration("Infrastructure:Persistence:Postgres"))
.WithMigrationsFrom(typeof(IAssemblyMarker).Assembly)
.WithDataSourcePlugin<MappingPlugin>()));У конфигуратора есть метод расширения UsePostgres, который настраивает подключение к PostgreSql. В метод так же передаётся
делегат, в котором можно параметризовать работу с базой с помощью методов:
WithConnectionOptions- настройка конфигурации подключения к БД.WithMigrationsFrom- указание сборки(-ок) с миграциями. Также можно использоватьWithMigrationsFromItemsдля указания конкретных типов с миграциями.WithDataSourcePlugin- подключение плагинов дляNpgsqlDataSource. Используемые плагины должны реализовыватьIPostgresDataSourcePlugin, и фактически позволяют дополнительно настроитьNpgsqlDataSourceBuilderперед созданием data source.
Параметры подключения к БД задаются через PostgresConnectionOptions.
{
"Host": string,
"Port": int,
"Database": string,
"Username": string,
"Password": string,
"SslMode": string,
"Pooling": bool,
"MaximumPoolSize": int,
"EnableConnectionProviderLogging": bool
}- Host
Адрес хоста PostgreSql. Обязательный параметр, не может быть пустым. - Port
Порт PostgreSql. Обязательный параметр, не может быть пустым или равным 0. Допустимые значения:1-65535. - Database
Название базы данных. Обязательный параметр, не может быть пустым. - Username
Имя пользователя. Обязательный параметр, не может быть пустым. - Password
Пароль. Обязательный параметр, не может быть пустым. - SslMode
Режим SSL-соединения. Необязательный параметр, по умолчанию пустая строка. Допустимые значения аналогичны Npgsql (документация):Disable,Allow,Prefer,Require,VerifyCA,VerifyFull. - Pooling
Использование пула соединений (connection pooling). Необязательный параметр, по умолчаниюtrue. - MaximumPoolSize
Максимальный размер пула соединений. Необязательный параметр, по умолчанию10, должен быть >=1. - EnableConnectionProviderLogging
Включение логирования. Необязательный параметр, по умолчаниюfalse. Для корректной работы необходимо также добавить платфморменный observability - в DI контейнере вызвать метод.AddPlatformObservability
Валидация значений происходит при старте сервиса. При невалидных значениях сервис не запустится.
{
"Infrastructure": {
"Persistence": {
"Postgres": {
"Host": "localhost",
"Database": "test-db",
"Port": 6432,
"Username": "postgres",
"Password": "postgres",
"SslMode": "Prefer",
"Pooling": true,
"MaximumPoolSize": 10,
"EnableConnectionProviderLogging": false
}
}
}
}Для получения соединений с БД используется IPersistenceConnectionProvider. с БД используется IPersistenceConnectionProvider.
Провайдер зарегистрирован в DI как Scoped, поэтому сервисы использующие его должны быть зарегистрированы как Scoped или Transient.
Чтобы получить объект соединения, необходимо вызвать метод провайдера GetConnectionAsync, который возвращает
IPersistenceConnection.
await using IPersistenceConnection connection = await _connectionProvider.GetConnectionAsync(cancellationToken);Команда создается через метод CreateCommand на объекте соединения. Параметры добавляются в fluent стиле:
await using IPersistenceCommand command = connection.CreateCommand(sql)
.AddParameter("parameter_1", value1)
.AddParameter("parameter_2", value2);Помимо вышеуказанного AddParameter<T>(string parameterName, T value), у IPersistenceConnection существуют другие методы и перегрузки для добавления параметров:
AddParameter(DbParameter parameter)- добавляет сыройDbParameter, фактически параметр должен быть типаNpgsqlParameter.AddParameter<T>(string parameterName, T value)- добавляет именованный параметр произвольного типа.AddParameter<T>(string parameterName, IEnumerable<T> values)- добавляет именованный параметр-список. Несмотря на ограничение Npgsql в передаваемом типе коллекции для параметра (List<T>илиT[]), в данный метод можно передавать любой тип, реализующийIEnumerable<T>. В sql коде удобно подставлять в= any(...)илиin (...).AddJsonParameter<T>(string parameterName, T value, JsonSerializerSettings? serializerSettings = null)- сериализует переданное значение в json и добавляет как параметр с типомjsonb.AddNullableJsonParameter<T>(string parameterName, T? value, JsonSerializerSettings? serializerSettings = null)- аналогичноAddJsonParameter, но поддерживаетnullзначения.AddJsonArrayParameter<T>(string parameterName, IEnumerable<T> values, JsonSerializerSettings? serializerSettings = null)- сериализует каждый элемент коллекции в json и добавляет как параметр с типомjsonb[].AddJsonArrayParameter(string parameterName, IEnumerable<string> values)- добавляет коллекцию строк (уже сериализованных объектов) как параметр с типомjsonb[].AddMultiArrayStringParameter<T>(string parameterName, IEnumerable<IEnumerable<T>> values)- позволяет добавлять многомерные массивы в качестве параметров. Каждый элемент коллекции 1-го уровня преобразуется в postgres-массив, затем итоговый список добавляется как параметр с типомtext[]. Для корректной работы в sql, необходимо явно привести каждый элемент списка к ожидаемому postgres-типу (например,integer[]илиbigint[]).AddMultiArrayStringParameter(string parameterName, IEnumerable<IEnumerable<string>> values)- аналогичноAddMultiArrayStringParameter, но для строковых значений коллекции.
При попытке добавить параметр с уже существующим названием будет выброшен
PlatformPersistencePostgresException.
Чтение данных реализовано через метод команды ExecuteReaderAsync, аналогично NpgsqlCommand:
await using DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
yield return new Model(
Id: reader.GetInt64("field_1"),
Name: reader.GetString("field_2"));
}Выполнение не читающих запросов (DML) реализовано через метод ExecuteNonQueryAsync, так же аналогично NpgsqlCommand:
await command.ExecuteNonQueryAsync(cancellationToken);Для управления транзакциями используется IPersistenceTransactionProvider, зарегистрированный в DI как Scoped.
Соответственно сервисы, использующие его, должны быть зарегистрированы как Scoped или Transient.
Для открытия транзакции используется метод BeginTransactionAsync с указанием уровня изоляции.
Метод возвращает IPersistenceTransaction - объект транзакции, который после выполнения операций необходимо закоммитить
(вызвать метод CommitAsync). Все операции, выполненные до вызова этого метода, автоматически попадают в транзакцию.
Если CommitAsync не был вызван, транзакция откатывается при DisposeAsync.
await using IPersistenceTransaction transaction = await _transactionProvider
.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken);
// выполнение операций через репозитории
await transaction.CommitAsync(cancellationToken);Фактически реализация IPersistenceTransaction использует под капотом TransactionScope с указанием параметра scopeOption: TransactionScopeOption.Required,
тем самым представляя собой ту же ambient транзакцию.
Механизм миграций реализован на базе библиотеки FluentMigrator.
Для написания миграции необходимо наследовать абстрактный класс SqlMigration, в котором определены методы GetUpSql и GetUpSql.
GetUpSql - sql-скрипт самой миграции, GetDownSql - скрипт отката
[Migration(1, "Initial")]
public class Initial : SqlMigration
{
protected override string GetUpSql(IServiceProvider serviceProvider)
{
return """
create table example
(
id bigserial not null primary key,
name text not null
);
""";
}
protected override string GetDownSql(IServiceProvider serviceProvider)
{
return """
drop table example;
""";
}
}Миграции запускаются автоматически при старте приложения. Сборка с миграциями указывается при регистрации в DI, методе WithMigrationsFrom.