Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Itmo.Dev.Platform.sln
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Itmo.Dev.Platform.Testing.B
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Itmo.Dev.Platform.Testing.Behavioural.MessagePersistence", "src\Itmo.Dev.Platform.Testing.Behavioural.MessagePersistence\Itmo.Dev.Platform.Testing.Behavioural.MessagePersistence.csproj", "{769371D9-527B-4C23-B45A-04CFC0C653B6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{18C776DA-A728-44D2-96B1-C79CD8312D9D}"
ProjectSection(SolutionItems) = preProject
docs\persistence.md = docs\persistence.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down
35 changes: 1 addition & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,6 @@
- [Itmo.Dev.Platform.Common](src/Itmo.Dev.Platform.Common/README.md)
- [Itmo.Dev.Platform.YandexCloud](src/Itmo.Dev.Platform.YandexCloud/README.md)
- [Itmo.Dev.Platform.Kafka](src/Itmo.Dev.Platform.Kafka/README.md)
- [Itmo.Dev.Platform.Postgres](src/Itmo.Dev.Platform.Postgres/README.md)
- [Itmo.Dev.Platform.Persistence](docs/persistence.md)
- [Itmo.Dev.Platform.Logging](src/Itmo.Dev.Platform.Logging/README.md)
- [Itmo.Dev.Platform.BackgroundTasks](src/Itmo.Dev.Platform.BackgroundTasks/README.md)

## Package source installation

1. Add `nuget.config` file next to your `*.sln` file (if one does not exists)
2. Prepare your GitHub personal access token with `packages:read` permission
3. Add reference to `itmo-is-dev` GitHub nuget repository
```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="github" value="https://nuget.pkg.github.com/itmo-is-dev/index.json"/>
</packageSources>
<packageSourceMapping>
<packageSource key="github">
<package pattern="Itmo.Dev.Platform*" />
</packageSource>
</packageSourceMapping>
</configuration>
```
4. When prompted for authorization, use your github username as username, and generated PAT as password

Alternatively, you can use CLI to add package source. Code below will add source into your global config.
If you want to add it into local config, you should run it with option `--configfile nuget.config`

```shell
dotnet nuget add source --username YOUR_USERNAME --password YOUR_GITHUB_PAT --store-password-in-clear-text --name github "https://nuget.pkg.github.com/itmo-is-dev/index.json"
```

### <p style="color: red"> WARNING! </p>

Adding source into local config, will result in credentials be written into local file \
Be aware that it may lead to credentials leaking into VSC \
You can store credentials separately from source definition, in global nuget.config
215 changes: 215 additions & 0 deletions docs/persistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
## Itmo.Dev.Platform.Persistence

Библиотека для работы с базами данных. Включает в себя абстракции для подключения к БД, выполнения запросов, управления транзакциями и миграциями.

## Подключение

Библиотека состоит из двух Nuget пакетов: `Itmo.Dev.Platform.Persistence.Abstractions` с абстракциями и
`Itmo.Dev.Platform.Persistence.Postgres` с реализациями для БД Postgres.

Для регистрации в DI-контейнере необходимо вызвать метод `AddPlatformPersistence`, предварительно зарегистрировав саму
платформу (метод `AddPlatform`).
Метод принимает делегат, в котором происходит конфигурация Persistence слоя.

```csharp
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`.

### Схема конфигурации

```json
{
"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 ([документация](https://www.npgsql.org/doc/api/Npgsql.SslMode.html)): `Disable`, `Allow`, `Prefer`, `Require`, `VerifyCA`, `VerifyFull`.
- **Pooling**
Использование пула соединений (connection pooling). Необязательный параметр, по умолчанию `true`.
- **MaximumPoolSize**
Максимальный размер пула соединений. Необязательный параметр, по умолчанию `10`, должен быть >= `1`.
- **EnableConnectionProviderLogging**
Включение логирования. Необязательный параметр, по умолчанию `false`.
Для корректной работы необходимо также добавить платфморменный observability - в DI контейнере
вызвать метод `.AddPlatformObservability`

> Валидация значений происходит при старте сервиса. При невалидных значениях сервис не запустится.

### Configuration example

```json
{
"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`.

```csharp
await using IPersistenceConnection connection = await _connectionProvider.GetConnectionAsync(cancellationToken);
```

### Создание и настройка команды

Команда создается через метод `CreateCommand` на объекте соединения. Параметры добавляются в fluent стиле:

```csharp
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`:

```csharp
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`:

```csharp
await command.ExecuteNonQueryAsync(cancellationToken);
```

## Транзакции

Для управления транзакциями используется `IPersistenceTransactionProvider`, зарегистрированный в DI как Scoped.
Соответственно сервисы, использующие его, должны быть зарегистрированы как Scoped или Transient.

Для открытия транзакции используется метод `BeginTransactionAsync` с указанием уровня изоляции.
Метод возвращает `IPersistenceTransaction` - объект транзакции, который после выполнения операций необходимо закоммитить
(вызвать метод `CommitAsync`). Все операции, выполненные до вызова этого метода, автоматически попадают в транзакцию.

Если `CommitAsync` не был вызван, транзакция откатывается при `DisposeAsync`.

```csharp
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` - скрипт отката

```csharp
[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`.
4 changes: 3 additions & 1 deletion src/Itmo.Dev.Platform.Persistence.Abstractions/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# Itmo.Dev.Platform.Persistence.Abstractions
# Itmo.Dev.Platform.Persistence.Abstractions

Документация: [README.md](https://github.com/itmo-is-dev/platform/blob/master/docs/persistence.md)
4 changes: 3 additions & 1 deletion src/Itmo.Dev.Platform.Persistence.Postgres/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# Itmo.Dev.Platform.Persistence.Postgres
# Itmo.Dev.Platform.Persistence.Postgres

Документация: [README.md](https://github.com/itmo-is-dev/platform/blob/master/docs/persistence.md)
Loading