Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/klestovalexej/wattle.examples
Примеры использования Wattle
https://github.com/klestovalexej/wattle.examples
array async batch-processing cache code-generation domain-driven-design domainobject idempotency idempotency-key identity inmemory-cache lock memory-cache partitions performance postgresql sql-server telemetry unitofwork unlimited
Last synced: 9 days ago
JSON representation
Примеры использования Wattle
- Host: GitHub
- URL: https://github.com/klestovalexej/wattle.examples
- Owner: KlestovAlexej
- License: mit
- Created: 2022-01-30T10:12:39.000Z (almost 3 years ago)
- Default Branch: master
- Last Pushed: 2024-10-29T22:32:59.000Z (20 days ago)
- Last Synced: 2024-11-06T11:12:33.618Z (13 days ago)
- Topics: array, async, batch-processing, cache, code-generation, domain-driven-design, domainobject, idempotency, idempotency-key, identity, inmemory-cache, lock, memory-cache, partitions, performance, postgresql, sql-server, telemetry, unitofwork, unlimited
- Language: C#
- Homepage: https://www.nuget.org/packages?q=Acme.Wattle
- Size: 1.42 MB
- Stars: 3
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# Wattle.Examples
Примеры использования фреймворка для создания высокопроизводительных серверов с доменными объектами.
Фреймворк кроссплатформенный, написан 100% на [C#](https://ru.wikipedia.org/wiki/C_Sharp) под [.NET 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0).
Пакеты **nuget** начинаются с префикса [Acme.Wattle](https://www.nuget.org/packages?q=Acme.Wattle)
Полнофункциональный [демонстрационный сервер](https://github.com/KlestovAlexej/Wattle.DemoServer) на базе библиотеки.
# Содержание
- [Как запускать примеры](#как-запускать-примеры)
- [**Идемпотентность**](#идемпотентность)
- [Пример старта реестра в БД на **1.000.000.000** уникальных ключей в RAM за **17 секунд**](#пример-старта-реестра-на-1000000000-уникальных-ключей-1712-секунд)
- [**Лок-объекты**](#лок-объекты)
- [Контейнеры](#контейнеры)
- [**Массивы бесконечной длины**](#массивы-бесконечной-длины)
- [**Пакетная обработка задач**](#пакетная-обработка-задач)
- [Телеметрия приложения](#телеметрия-приложения)
- [Доменные объекты](#доменные-объекты)
- [Паттерн **Unit of Work**](#паттерн-unit-of-work)
- [Автогенерация мапперов на чистом ADO.NET для PostgreSQL и SQL Server](#автогенерация-мапперов-на-чистом-adonet)
- [Кодогенерация мапперов](#кодогенерация-мапперов)
- [Основные возможности](#основные-возможности)
- [**Генерация уникального** персистентного в БД значения **первичного ключа** с минимальным обращением к БД](#генерация-уникального-персистентного-в-бд-значения-первичного-ключа-с-минимальным-обращением-к-бд)
- [Результат параллельного создания **50.000.000** уникальных первичных ключей **(33,7 секунды)**](#результат-параллельного-создания-50000000-уникальных-первичных-ключей-337-секунды)
- [**Кэширование записей** по первичному ключу](#кэширование-записей-по-первичному-ключу)
- [Результат параллельного чтения **10.000.000** записей БД по первичному ключу **(9,3 секунд)**](#результат-параллельного-чтения-10000000-записей-бд-по-первичному-ключу-93-секунд)
- [Поддержка **партиционирования PostgreSQL** из коробки](#поддержка-партиционирования-postgresql-из-коробки)
---
## Как запускать примеры
Все примеры оформлены как [NUnit](https://nunit.org/)-тесты для запуска в ОС Windows из-под [Visual Studio 2022](https://visualstudio.microsoft.com/ru/vs/) (проверено на версии 17.8.1).Все БД в примерах создаются и уничтожаются автоматически при запуске теста.
Параметры подключения к серверу БД надо настроить в файле [DbCredentials.cs](/Common/DbCredentials.cs) и примеры 100% готовы к запуску.
Базы данных на которых проверялись примеры [PostgreSQL (15.1 и 16.0)](https://www.postgresql.org/) и [SQL Server 2019 Standard](https://www.microsoft.com/ru-ru/sql-server/sql-server-2019).
---
## Идемпотентность[Идемпотентность](https://ru.wikipedia.org/wiki/%D0%98%D0%B4%D0%B5%D0%BC%D0%BF%D0%BE%D1%82%D0%B5%D0%BD%D1%82%D0%BD%D0%BE%D1%81%D1%82%D1%8C) поддерживается готовыми компонентами - *реестры уникальных ключей* из nuget пакета [Acme.Wattle.UniqueRegisters](https://www.nuget.org/packages/Acme.Wattle.UniqueRegisters/).
Реестры уникальных ключей реализуют:
- Хранение ключей в оперативной памяти и персистентно в БД.
- Ключи могут быть ассоциированы с произвольными данными, тоже хранимыми в памяти.
- Поиск ключа или данных по ключу происходит только в оперативной памяти и не задействует БД.
- Параллельная регистрация одного и того же уникального ключа не приводит к задвоениям или рассинхронизациям с БД.
- Есть сборка мусора - устаревшие ключи и данные автоматически удаляются из оперативной памяти по критерию заданным программистом.
- Адаптирован для применения партиционирования PostgreSQL таблицы ключей для изятия устаревших ключей из памяти и БД.
- Хранение ключей и данных в оперативной памяти оптимизировано и не нагружает сборщик мусора.
- Ключи и данные в оперативной памяти занимают места *O(_Число ключей * (размер ключа + размер данных)_)* и не расходуют память на служебную информацию CLR.
- Регистрация нового ключа задействует БД только в момент подтверждения [Unit of Work](https://martinfowler.com/eaaCatalog/unitOfWork.html).
- Регистрация нового ключа отменяется автоматически в аварийных ситуациях работы Unit of Work.
В проекте [UniqueRegisters.Examples](https://github.com/KlestovAlexej/Wattle.Examples/tree/master/UniqueRegisters/Examples) весь код примеров.### Пример старта реестра на **1.000.000.000** уникальных ключей *(17,12 секунд)*
В данном примере в БД создано 1.000.000.000 условных ключей транзакци и полезной нагрузки.
На данной БД запускается реестр уникальных ключей для загрузки всех ключей в память.
Ключ : 16 байт [Guid](https://docs.microsoft.com/ru-ru/dotnet/api/system.guid?view=net-6.0)
Данные : 8 байт [Long](https://docs.microsoft.com/ru-ru/dotnet/api/system.int64?view=net-6.0)
Тест [Example_Start](https://github.com/KlestovAlexej/Wattle.Examples/blob/master/UniqueRegisters/Examples/Examples.cs#L67) из примеров [Examples.cs](https://github.com/KlestovAlexej/Wattle.Examples/blob/master/UniqueRegisters/Examples/Examples.cs).
| Действие | Результат |
| --- | --- |
| Время создания и инициализации реестра | 17,12 секунды |
| Занято памяти | 24.000.662.440 байт |
| Среднее время поиска данных по ключу | 16,92 микросекунды |
Полный лог примера:```
Текущее время тестов : 24.11.2023 1:00:00
Настройки сервера PostgreSQL (файл 'postgresql.conf').
enable_partition_pruning = on # должно быть 'on'----------------------------------------------------------------------
Создание ключей в БД.Время создания ключей : 02:13:07.2141639
Всего ключей : 1 000 000 000Всего занято памяти тестом : 24 004 924 304 байт
Занято памяти реестром : 24 000 388 640 байтКоличество созданных Unit of Works : 0
Количество реальных подключений к БД : 16 745
Количество реальных транзакций БД : 16 705
Количество сессий мапперов : 16 745Число зарегестрированных ключей : 1 000 000 000
Число регистраций ключей :
Количество загрузок групп ключей из персистентного хранилища : 19
Количество сохранений групп ключей в персистентное хранилище : 1Содержимого файлового кэша для быстрого старта 'C:\Dev\Wattle.Examples\UniqueRegisters\Examples\bin\Release\net8.0-windows\win-x64\Data\TransactionKeys' (файлов 20) :
gkeys_TransactionKeys_358.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_361.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_364.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_367.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_370.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_373.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_376.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_379.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_382.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_385.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_388.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_391.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_394.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_397.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_400.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_403.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_406.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_409.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_412.gkeys - размер 1 200 001 958
gkeys_TransactionKeys_415.gkeys - размер 1 200 001 958----------------------------------------------------------------------
Старт реестра на '1 000 000 000' ключах в БД и файловым кэше.
Время создания и инициализации реестра : 00:00:17.1194485Время поиска всех ключей : 04:41:56.4193987
Среднее время поиска ключа (микросекунд) : 16,9164193987
Всего занято памяти тестом : 24 005 521 040 байт
Занято памяти реестром : 24 000 662 440 байт
Количество созданных Unit of Works : 1 000 000 000
Количество реальных подключений к БД : 1
Количество реальных транзакций БД : 0
Количество сессий мапперов : 1
Число зарегестрированных ключей : 1 000 000 000
Число регистраций ключей : 0
Количество загрузок групп ключей из персистентного хранилища : 20
Количество сохранений групп ключей в персистентное хранилище : 0----------------------------------------------------------------------
Полное время теста : 06:55:21.4239389
```Параметры ПК :
_OS Windows 11 Pro x64, CPU Intel Core i9-9900KS, RAM 48GB, SSD Samsung 970 Evo Plus 2Tb, DB PostgreSQL 16.0_---
## Лок-объектыВ проекте [Locks](/Locks) весь код примеров.
Лок-объекты позволяют получать монопольную блокировку используя для блокировки любой объект применимый в качестве ключа [Dictionary](https://learn.microsoft.com/ru-ru/dotnet/api/system.collections.generic.dictionary-2?view=net-7.0).
К примеру, на C# так блокировку получить нельзя ([описание проблемы](https://stackoverflow.com/questions/1329919/why-lockinteger-var-is-not-allowed-but-monitor-enterinteger-var-allowed)):```csharp
lock(123)
{
...
}
```
Лок-объекты позволяют это делать :```csharp
using var lockObject = locks.GetLock(123);
if (lockObject.TryEnter())
{
...
}
```
Лок-объекты позволяют получать монопольную блокировку используя конструкцию [await](https://learn.microsoft.com/ru-ru/dotnet/csharp/language-reference/operators/await) :```csharp
using var lockObject = locks.GetLock(123);
if (await lockObject.TryEnterAsync(cancellationToken))
{
...
}
```---
## КонтейнерыВ проекте [Containers](/Containers) весь код примеров.
---
### Массивы бесконечной длиныМассивы бесконечной длины позволяют:
- Cоздавать массивы (любых элементов) неограниченной длинны, на **всю доступную** оперативную память.
- Добавлять дополнительную память массиву без копирования памяти.---
Тест [Example_Byte_Compare](/Containers/ExamplesFlexIncrementArrayElements.cs#L87) из примеров [ExamplesFlexIncrementArrayElements.cs](/Containers/ExamplesFlexIncrementArrayElements.cs).
Сравнивает работу **массива бесконечной длины** с **[byte](https://learn.microsoft.com/ru-ru/dotnet/api/system.byte?view=net-7.0)[]**.
Полный лог теста :```
Тест FlexIncrementArrayElementsЭлементов массива : 100 000 000
Выделено памяти (байт) : 100 000 000
Время : 00:00:00.0042149Время заполнения памяти : 00:00:00.2242565
Время проверки памяти : 00:00:20.1773304
Время проверки памяти : 00:01:28.1654554----------------------------------------------------------------------
Эталонный тест byte[]
Элементов массива : 100 000 000
Выделено памяти (байт) : 100 000 000
Время : 00:00:00.0001068Время заполнения памяти : 00:00:00.0519848
Время проверки памяти : 00:00:22.0954823
Время проверки памяти : 00:01:27.0711296
```---
Тест [Example_Byte_Expand](/Containers/ExamplesFlexIncrementArrayElements.cs#L31) из примеров [ExamplesFlexIncrementArrayElements.cs](/Containers/ExamplesFlexIncrementArrayElements.cs).
Cоздание массив из **30.000.000.000** элементов типа [byte](https://learn.microsoft.com/ru-ru/dotnet/api/system.byte?view=net-7.0).
Полный лог теста :```
Элементов массива типа byte : 30 000 000 000
Выделено памяти (байт) : 30 001 001 616
Время : 00:00:01.1994014
```Параметры ПК :
_OS Windows 11 Pro x64, CPU Intel Core i9-9900KS, RAM 48GB, SSD Samsung 970 Evo Plus 2Tb_---
## Пакетная обработка задач
Класс **BaseBatchingTasksProcessor** позволяет реализовать стратегию пакетной обработки задач.
В проекте [BatchingTasks](/BatchingTasks) весь код примеров.
**Пример проблемы и её решение :**
Есть сервер, который принимает множество подключений.
Каждое принятое подключение исполняет задачу на сервера, к примеру делает однотипный [INSERT](https://postgrespro.ru/docs/postgresql/9.6/sql-insert) в БД.
Логично делать [INSERT](https://postgrespro.ru/docs/postgresql/9.6/sql-insert) в БД не штучно, а использовать [BULK INSERT](https://www.postgresql.org/docs/current/sql-copy.html) для большей производительности вставки.
Возникает проблема общения задач всех подключений в единый [BULK INSERT](https://www.postgresql.org/docs/current/sql-copy.html).
Помимо этого, каждое подключение должно получить уведомление что задача выполнена.
Решением проблемы занимается класс **BaseBatchingTasksProcessor**, его возможности :
- Алгоритм обработки пакета задач польностью определяет программист.
- Принимать для обработки объекты-задачи и автоматически объединять их в пакеты для обработки.
- Обработка задач в пакете идёт асинхронно.
- Есть возможность ожидать конца исполнения задачи или вообще не ждать конца исполнения задачи.
- Ожидание конца исполнения задачи может быть с использованием конструкции [await](https://learn.microsoft.com/ru-ru/dotnet/csharp/language-reference/operators/await).
- Если при обработке пакета была ошибка, она будет доведена до всех ожидающих конца исполнения задачи.
- Количество задач в пакете можно ограничить
- Количество исполняемых пакетов можно ограничить
- Время ожидания выполнения задачи можно выбрать произвольное или ждать бесконечно
Пример добавления задачи и ожидание конца её исполнения:
```csharp
using var processor = new BatchingTasksProcessor(...);var task =
new BatchingTask
{
...
};var waitHandle = processor.Process(task);
waitHandle.Wait();
```---
## Телеметрия приложенияПростая публикация и доступ по REST-интерфейсу к произвольной телеметрии приложения.
Это не замена и не конкурент [OpenTelemetry](https://opentelemetry.io/).
В проекте [InfrastructureMonitoring](/InfrastructureMonitoring) весь код примера.
---
Клаcc приложения с телеметрией для публикации по REST-интерфейсу:
```csharp
public class CustomClassA
{
...// Телеметрия.
public long Count;
}
```[Swagger](https://swagger.io/) документация REST-интерфейса доступа к телеметрии приложения:
```csharp
http://localhost:5601/swagger/index.html
```---
Получение значение телеметрии через REST-интерфейс используя C# и готовый клиент:
```csharp
using var client = new InfrastructureMonitorClient("localhost", 5601);var snapshot = client.GetInfrastructureMonitorSnapshot(WellknownCustomInfrastructureMonitors.CustomClassA);
var snapShotValue = snapshot.Values.Single(v => v.Id == WellknownCustomSnapShotInfrastructureMonitorValues.CustomClassA.Count);
var count = (long)snapShotValue.Data.Value;Console.WriteLine(count);
```---
Получение значение телеметрии через REST-интерфейс используя PowerShell:
```ps1
# Идентификатор объекта приложения с телеметрией - WellknownCustomInfrastructureMonitors.CustomClassA
$MonitorId = "153C867D-A122-44BB-B689-949FB8C61B00"# Идентификатор значения с телеметрией - WellknownCustomSnapShotInfrastructureMonitorValues.CustomClassA.Count
$ValueId = "50FF7F28-582B-4297-93EE-323FB812880F"$Result = Invoke-WebRequest -Uri "http://localhost:5601/api/1/InfrastructureMonitor/GetInfrastructureMonitorSnapshotValue?monitorId=$MonitorId&valueId=$ValueId" -Method Get
Write-Host ($Result)
```Полученное значение телеметрии:
```json
{
"Id": "153c867d-a122-44bb-b689-949fb8c61b00",
"Name": "Класс CustomClassA",
"Description": "Класс CustomClassA",
"SnapShotTime": "2022-01-31T15:12:48.6279777+03:00",
"Value": {
"Id": "50ff7f28-582b-4297-93ee-323fb812880f",
"Name": "Количество элементов",
"Description": "Количество элементов",
"Data": {
"Value": 11,
"Type": "long"
},
"Group": null,
"Tags": [
"COUNT"
]
}
}
```---
## Доменные объекты
Фреймворк позволяет программировать [доменные объекты](https://en.wikipedia.org/wiki/Business_object) и максимально сосредоточиться на бизнес-значимых понятиях не в ущерб производительности.В проекте [DomainObjects](/DomainObjects) весь код примеров.
---
### Паттерн Unit of Work
Работа с доменными объектами идёт в пределах [Unit of Work](https://martinfowler.com/eaaCatalog/unitOfWork.html) с контраком *IUnitOfWork*.
Сам Unit of Work создаётся точкой входа в доменную область с контраком *IEntryPoint*.Весь код примеров в файле [Examples.cs](DomainObjects/Examples/Examples.cs).
Контракт IEntryPoint :
```csharp
///
/// Точка входа в доменную область.
///
public interface IEntryPoint : IDisposable, IDrivenObject
{
///
/// Монитор инфраструктуры.
///
new IInfrastructureMonitorEntryPoint InfrastructureMonitor { get; }///
/// Провайдер экземпляра для вызывающего потока.
///
IUnitOfWorkProvider UnitOfWorkProvider { get; }///
/// Создание для вызывающего потока.
///
/// Конекст сосздания .
/// Созданный .
/// Если для вызывающего потока уже определён .
IUnitOfWork CreateUnitOfWork(object context = null);///
/// Создание для вызывающего потока.
///
/// Конекст сосздания .
/// Токен отмены.
/// Созданный .
/// Если для вызывающего потока уже определён .
ValueTask CreateUnitOfWorkAsync(object context = null, CancellationToken cancellationToken = default);
}
```Контракт IUnitOfWork :
```csharp
///
/// Сессия доменной области.
///
public interface IUnitOfWork : IHostMappersSession
{
///
/// Признак уничтожения .
///
bool IsDisposed { get; }///
/// Статус .
///
UnitOfWorkState State { get; }///
/// Произвольные ассоциированные данные.
///
IDictionary Tags { get; }///
/// Точка входа в доменную область.
///
IEntryPoint EntryPoint { get; }///
/// Реест локальных изменяемых состояний доменных объектов использованных в .
///
IDomainObjectMutableStatesRegister DomainObjectMutableStates { get; }///
/// Реестры доменных объектов.
///
IDomainObjectRegisters Registers { get; }///
/// Стратегия проверки успешности завершения .
///
IUnitOfWorkCommitVerifying CommitVerifying { get; set; }///
/// Стратегия проверки успешности завершения определена.
///
bool IsDefinedCommitVerifying { get; }///
/// Добавить доменное поведение в .
///
/// Доменное поведение.
void AddBehaviour(IDomainBehaviour domainBehaviour);///
/// Добавить новый доменный объект в (, ).
///
/// Доменный объект.
void AddNew(IDomainObject domainObject);///
/// Добавить изменённый доменный объект в (, ).
///
/// Доменный объект.
void AddUpdate(IDomainObject domainObject);///
/// Добавить удалённый доменный объект в (, ).
///
/// Доменный объект.
void AddDelete(IDomainObject domainObject);///
/// Добавить проверку верси данных доменного объект в (, ).
///
/// Доменный объект.
void AddVersion(IDomainObject domainObject);///
/// Подтвердить .
/// Вызов может быть только один.
///
/// - если были изменения. - если изменений не было или был отменён.
bool Commit();///
/// Подтвердить .
/// Вызов может быть только один.
///
/// Токен отмены.
/// - если были изменения. - если изменений не было или был отменён.
ValueTask CommitAsync(CancellationToken cancellationToken = default);///
/// Отменить .
/// Вызывать можно неограниченное количество раз.
///
void Rollback();///
/// Пометить как подозрительный для принудительного восстановления всех доменных объектов задействованных в .
/// Вызывать можно неограниченное количество раз.
///
void Suspect();
}
```Пример создания доменного объекта :
```csharp
IEntryPoint entryPoint;...
using (IUnitOfWork unitOfWork = entryPoint.CreateUnitOfWork())
{
var register = unitOfWork.Registers.GetRegister();
var instance = register.New(new DateTime(2022, 1, 2, 3, 4, 5, 6), 1002, 444);unitOfWork.Commit();
}
```---
## Автогенерация мапперов на чистом ADO.NETПримеры автогенерённых ADO.NET мапперов для [PostgreSQL](/Mappers/PostgreSql/Implements/).
Примеры автогенерённых ADO.NET мапперов для [SQL Server](/Mappers/SqlServer/Implements/).
---
### Определение для кодогенератора мапперовВесь код примера [WellknownDomainObjectFields.cs](/Mappers/PostgreSql/Common/WellknownDomainObjectFields.cs).
Пример определения структуры записи БД и параметров маппера :
```csharp
[Description("Объект Object_A")]
[SchemaMapper(MapperId = WellknownDomainObjects.Text.Object_A, IsPrepared = true, IsCached = true, DeleteMode = SchemaMapperDeleteMode.Delete)]
[SchemaMapperIdentityFieldPostgreSql(PartitionsLevel = ComplexIdentity.Level.L1)]
[SchemaMapperIdentityField(DbSequenceName = "Sequence_%ObjectName%")]
[SchemaMapperRevisionField(IsVersion = true)]
public static class Object_A
{
[Description("Дата-время (DateTime). Обновляемое поле.")]
[SchemaMapperField(typeof(DateTime), Where = true, Order = true, UpdateMode = SchemaMapperFieldUpdateMode.UpdateDirect)]
public static readonly Guid Value_DateTime = new("DCE071BB-796E-4397-91B8-EAF116747880");[Description("Дата-время (DateTime). Не обновляемое поле.")]
[SchemaMapperField(typeof(DateTime), Where = true, Order = true, UpdateMode = SchemaMapperFieldUpdateMode.NotUpdate)]
public static readonly Guid Value_DateTime_NotUpdate = new("273A65E2-7647-42DB-A15D-58B69A64C69D");[Description("Число (long). Поле обновляется только при изменении значения.")]
[SchemaMapperField(typeof(long), Where = true, Order = true, UpdateMode = SchemaMapperFieldUpdateMode.Update)]
public static readonly Guid Value_Long = new("87A005ED-CA51-4C60-83EC-6540AC0823D6");[Description("Число с поддержкой null (int?). Обновляемое поле.")]
[SchemaMapperField(typeof(int?), Where = true, Order = true, UpdateMode = SchemaMapperFieldUpdateMode.UpdateDirect)]
public static readonly Guid Value_Int = new("198251EF-8183-4A09-A760-E5BAAFBBB6FF");[Description("Строка без ограничения размера с поддержкой null. Поле обновляется только при изменении значения.")]
[SchemaMapperField(typeof(string), DbIsNull = true, UpdateMode = SchemaMapperFieldUpdateMode.Update)]
public static readonly Guid Value_String = new("100E6573-B387-4CB5-B3D6-45DF4CB2CC9C");
}
```Весь скрипт [SqlScript.sql](/Mappers/PostgreSql/Common/SqlScripts/SqlScript.sql).
SQL-скрипт создания таблицы и последовательности PostgreSQL :
```sql
CREATE SEQUENCE Sequence_Object_A START WITH 1 INCREMENT BY 1;CREATE TABLE object_a(
id bigint NOT NULL,
revision bigint NOT NULL,
value_datetime timestamp NOT NULL,
value_long bigint NOT NULL,
value_int integer,
value_string text,
value_datetime_notupdate timestamp NOT NULL,
PRIMARY KEY(id)
) PARTITION BY RANGE (id);
```---
### Создание XML-схемы мапперов по определениюВесь код примера [DbMappersSchemaXmlBuilder.cs](/Mappers/PostgreSql/Common/DbMappersSchemaXmlBuilder.cs).
Пример создания XML-схемы маппераPostgreSQL :
```csharp
var mappers =
new SchemaMappers
{
Storage = SchemaMapperStorage.PostgreSql,
CodeGeneration =
new SchemaMappersCodeGeneration
{
MappersCommonNamespaceName = "Acme.Wattle.Examples.Mappers.PostgreSql.Implements.Generated.Common",
MappersIntefacesNamespaceName = "Acme.Wattle.Examples.Mappers.PostgreSql.Implements.Generated.Interface",
MappersImplementsNamespaceName = "Acme.Wattle.Examples.Mappers.PostgreSql.Implements.Generated.Implements",
MappersTestsNamespaceName = "Acme.Wattle.Examples.Mappers.PostgreSql.Implements.Generated.Tests",
UnitTestCategoryName = TestCategory.Unit,
UnitTestTimeout = TestTimeout.Unit,
},
};var schemaModel =
new SchemaModel
{
Description = $"Генератор XML модели тут : {GetType().FullName}",
Mappers = new List { mappers }
};var type = typeof(Object_A);
var schemaMapperBuilder = SchemaMapperBuilder
.New()
.SetSchema(mappers.Storage)
.Configure(type);
var schemaMapper = schemaMapperBuilder.CreateSchema(mappers.Storage);
mappers.Mappers.Add(schemaMapper);var xml = schemaModel.ToXml();
var fileName = Path.Combine(ProviderProjectBasePath.ProjectPath, @"Mappers\PostgreSql\Implements\DbMappers.Schema.xml");
File.WriteAllText(fileName, xml);
```---
### Кодогенерация мапперовВесь примера в файле [Mappers.PostgreSql.Implements.csproj](/Mappers/PostgreSql/Implements/Mappers.PostgreSql.Implements.csproj).
Пример проектного файла :
```xml
...
all
runtime; build; native; contentfiles; analyzers; buildtransitive
all
runtime; build; native; contentfiles; analyzers; buildtransitive
all
runtime; build; native; contentfiles; analyzers; buildtransitive
all
runtime; build; native; contentfiles; analyzers; buildtransitive
```
Весь примера в файле [DbMappers.Interfaces.Generated.cs](/Mappers/PostgreSql/Implements/GeneratedFiles/Acme.Wattle.CodeGeneration.Generator.Interfaces/Acme.Wattle.CodeGeneration.Generator.Interfaces.SourceGenerator/DbMappers.Interfaces.Generated.cs).
Пример сгенерированного интерфейса маппера :
```csharp
///
/// Объект с партиционированием таблицы БД, первичным ключём из последовательности БД, с оптимистической конкуренцией на уровне БД, с кешированием записей БД в памяти на уровне маппера
///
[MapperInterface(WellknownMappersAsText.Object_A)]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
// ReSharper disable once PartialTypeWithSinglePart
public partial interface IMapperObject_A : IMapper
{
///
/// Имя таблицы БД.
///
string TableName { get; }///
/// Получение следующего значения идентити из последовательности "Sequence_Object_A".
///
/// Сессия БД.
/// Возвращает следующего значение идентити.
long GetNextId(IMappersSession session);///
/// Получение следующих значений идентити из последовательности "Sequence_Object_A".
///
/// Сессия БД.
/// Количество следующийх значений идентити из последовательности.
/// Кокен отмены.
/// Возвращает коллекцию следующих значений идентити.
IList GetNextIds(IMappersSession session, int count, CancellationToken cancellationToken);///
/// Получение следующего значения идентити из последовательности "Sequence_Object_A".
///
/// Сессия БД.
/// Токен отмены.
/// Возвращает следующего значение идентити.
ValueTask GetNextIdAsync(IMappersSession session, CancellationToken cancellationToken = default);///
/// Проверка существования записис с указаным идентити.
///
/// Сессия БД.
/// Идентити записи.
/// Возвращает если запись существует иначе если запись не существует возвращает .
bool Exists(IMappersSession session, long id);///
/// Проверка существования записис с указаным идентити.
///
/// Сессия БД.
/// Идентити записи.
/// Токен отмены.
/// Возвращает если запись существует иначе если запись не существует возвращает .
ValueTask ExistsAsync(IMappersSession session, long id, CancellationToken cancellationToken = default);///
/// Проверка существования записис с указаным идентити.
///
/// Сессия БД.
/// Идентити записи.
/// Возвращает если запись существует иначе если запись не существует возвращает .
bool ExistsRaw(IMappersSession session, long id);///
/// Проверка существования записис с указаным идентити.
///
/// Сессия БД.
/// Идентити записи.
/// Токен отмены.
/// Возвращает если запись существует иначе если запись не существует возвращает .
ValueTask ExistsRawAsync(IMappersSession session, long id, CancellationToken cancellationToken = default);///
/// Проверка существования записис с указаным идентити и указаной версией данных.
///
/// Сессия БД.
/// Идентити записи.
/// Ожидаемая версия данных записи.
/// Возвращает если запись существует иначе если запись не существует возвращает .
bool ExistsRevision(IMappersSession session, long id, long revision);///
/// Проверка существования записис с указаным идентити и указаной версией данных.
///
/// Сессия БД.
/// Идентити записи.
/// Ожидаемая версия данных записи.
/// Токен отмены.
/// Возвращает если запись существует иначе если запись не существует возвращает .
ValueTask ExistsRevisionAsync(IMappersSession session, long id, long revision, CancellationToken cancellationToken = default);///
/// Получить запись с указаным идентити.
///
/// Хост сессии БД.
/// Идентити записи.
/// Возвращает значение если запись существует иначе если запись не существует возвращает .
Object_ADtoActual Get(IHostMappersSession mappersSession, long id);///
/// Получить запись с указаным идентити.
///
/// Хост сессии БД.
/// Идентити записи.
/// Токен отмены.
/// Возвращает значение если запись существует иначе если запись не существует возвращает .
ValueTask GetAsync(IHostMappersSession mappersSession, long id, CancellationToken cancellationToken = default);///
/// Получить запись с указаным идентити.
///
/// Сессия БД.
/// Идентити записи.
/// Возвращает значение если запись существует иначе если запись не существует возвращает .
Object_ADtoActual GetRaw(IMappersSession session, long id);///
/// Получить запись с указаным идентити.
///
/// Сессия БД.
/// Идентити записи.
/// Токен отмены.
/// Возвращает значение если запись существует иначе если запись не существует возвращает .
ValueTask GetRawAsync(IMappersSession session, long id, CancellationToken cancellationToken = default);///
/// Обновить запись.
///
/// Сессия БД.
/// Измененная запись.
/// Возвращает актуальное состояние записи.
Object_ADtoActual Update(IMappersSession session, Object_ADtoChanged data);///
/// Обновить запись.
///
/// Сессия БД.
/// Измененная запись.
/// Токен отмены.
/// Возвращает актуальное состояние записи.
ValueTask UpdateAsync(IMappersSession session, Object_ADtoChanged data, CancellationToken cancellationToken = default);///
/// Массовое создание записей.
///
/// Сессия БД.
/// Записи.
void BulkInsert(IMappersSession session, IEnumerable data);///
/// Массовое создание записей.
///
/// Сессия БД.
/// Записи.
/// Токен отмены.
ValueTask BulkInsertAsync(IMappersSession session, IEnumerable data, CancellationToken cancellationToken = default);///
/// Создать запись.
///
/// Сессия БД.
/// Новая запись.
/// Возвращает актуальное состояние записи.
Object_ADtoActual New(IMappersSession session, Object_ADtoNew data);///
/// Создать запись.
///
/// Сессия БД.
/// Новая запись.
/// Токен отмены.
/// Возвращает актуальное состояние записи.
ValueTask NewAsync(IMappersSession session, Object_ADtoNew data, CancellationToken cancellationToken = default);///
/// Удаление записи.
///
/// Сессия БД.
/// Данные достаточные для удаления записи.
void Delete(IMappersSession session, Object_ADtoDeleted data);///
/// Удаление записи.
///
/// Сессия БД.
/// Данные достаточные для удаления записи.
/// Токен отмены.
ValueTask DeleteAsync(IMappersSession session, Object_ADtoDeleted data, CancellationToken cancellationToken = default);///
/// Получить итератор всех записей выбранных с учётом фильтра.
///
/// Сессия БД.
/// Фильтр выбора записий. Если указан то выбираются все записи.
/// Возвращает итератор всех выбраных записей.
IEnumerable GetEnumerator(IMappersSession session, IMapperSelectFilter selectFilter = null);///
/// Получить итератор всех записей выбранных с учётом фильтра.
///
/// Сессия БД.
/// Фильтр выбора записий. Если указан то выбираются все записи.
/// Возвращает итератор всех выбраных записей.
IEnumerable GetEnumeratorRaw(IMappersSession session, IMapperSelectFilter selectFilter = null);///
/// Получить итератор записей выбранных с учётом фильтра для заданной страницы указанного размера.
///
/// Сессия БД.
/// Индекс выбираемой страницы. Первая страница имеет индекс 1.
/// Размер страницы. Минимальный размер страницы 1. Максимальный размер страницы 1000.
/// Фильтр выбора записий. Если указан то выбираются все записи.
/// Возвращает итератор всех выбраных записей.
IEnumerable GetEnumeratorPage(IMappersSession session, int pageIndex, int pageSize, IMapperSelectFilter selectFilter = null);///
/// Получить итератор идентити записей выбранных с учётом фильтра для заданной страницы указанного размера.
///
/// Сессия БД.
/// Индекс выбираемой страницы. Первая страница имеет индекс 1.
/// Размер страницы. Минимальный размер страницы 1. Максимальный размер страницы 1000.
/// Фильтр выбора записий. Если указан то выбираются все записи.
/// Возвращает итератор всех выбраных идентити записей.
IEnumerable GetEnumeratorIdentitiesPage(IMappersSession session, int pageIndex, int pageSize, IMapperSelectFilter selectFilter = null);///
/// Получить количество записей удовлетворяющих фильтру выборки.
///
/// Сессия БД.
/// Фильтр выбора записий. Если указан то выбираются все записи.
/// Возвращает количество записей удовлетворяющих фильтру выборки.
long GetCount(IMappersSession session, IMapperSelectFilter selectFilter = null);
}
```---
### Основные возможностиДля PostgreSQL в файле [Examples.cs](/Mappers/PostgreSql/Implements/Examples.cs) весь код примеров работы с мапперами.
Для SQL Server в файле [Examples.cs](/Mappers/SqlServer/Implements/Examples.cs) весь код примеров работы с мапперами.
---
#### Генерация уникального персистентного в БД значения первичного ключа с минимальным обращением к БДГенератор уникальных первичных ключей работает на базе последовательностей БД.
Генератор позволяет получать уникальные значения без необходимости реального обращения к БД в момент генерации.
Весь код примеров в тесте [Example_IdentityCache_Parallel](Mappers/PostgreSql/Implements/Examples.cs#L1086).
Пример параллельного создания 50.000.000 уникальных первичных ключей :
```csharp
const int CacheSize = 100_000;
using var identityCache = CreateIdentityCache(CacheSize);var identites = new HashSet();
Parallel.For(0, 500 * CacheSize,
_ =>
{
using var mappersSession = m_mappers.OpenSession();var identity = identityCache.GetNextIdentity(mappersSession);
lock (identites)
{
Assert.IsFalse(identites.Contains(identity));
identites.Add(identity);
}mappersSession.Commit();
});
```##### Результат параллельного создания **50.000.000** уникальных первичных ключей *(33,7 секунды)*
| Действие | Результат |
| --- | --- |
| Время работы | 33.7 секунды |
| Количество реальных подключений к БД | 2 |
| Количество идентити | 50.000.000 |
| Количество идентити полученных из кэша в памяти (без обращения к БД) | 49.990.310 |
| Количество идентити полученных из БД | 9.690 |
| Количество реальных подключений к БД | 10.454 |
| Количество сессий мапперов | 50.000.764 |Параметры ПК :
_OS Windows 11 Pro x64, CPU Intel Core i9-9900KS, RAM 48GB, SSD Samsung 970 Evo Plus 2Tb, DB PostgreSQL 15.1_---
#### Кэширование записей по первичному ключуКэширование записей по первичну ключу происходит автоматически (если это наcтроено для маппера).
У каждого маппера свой кэш.
Кэш обновляется и очищается автоматически при создании, чтении, обновлении или удалении записи.
Весь код примеров в тесте [Example_Select_With_MemoryCache_Parallel](Mappers/PostgreSql/Implements/Examples.cs#L672).
Пример параллельного чтения 10.000.000 записей БД по первичному ключу :
```csharp
var ids = new List();...
// Заполнение таблицы.
using (var mappersSession = m_mappers.OpenSession())
{
foreach (var id in ids)
{
mapper.New(
mappersSession,
new Object_ADtoNew
{
Id = id,
Value_DateTime = DateTime.Now,
Value_DateTime_NotUpdate = DateTime.Now,
Value_Int = null,
Value_Long = id,
Value_String = $"Text {id}",
});
}mappersSession.Commit();
}// Выборка записей по первичному ключу.
Parallel.For(0, 10_000_000,
_ =>
{
using var mappersSession = m_mappers.CreateHostMappersSession();var id = ProviderRandomValues.GetItem(ids);
var dto = mapper.Get(mappersSession, id);
Assert.IsNotNull(dto);
});
```##### Результат параллельного чтения **10.000.000** записей БД по первичному ключу *(9,3 секунд)*
| Действие | Результат |
| --- | --- |
| Время работы | 9,3 секунды |
| Количество реальных подключений к БД | 2 |
| Количество сессий мапперов | 2 |
| Количество объектов в памяти | 1.000 |
| Количество поисков объектов в памяти | 10.000.000 |
| Количество найденных объектов в памяти | 10.000.000 |Параметры ПК :
_OS Windows 11 Pro x64, CPU Intel Core i9-9900KS, RAM 48GB, SSD Samsung 970 Evo Plus 2Tb, DB PostgreSQL 15.1_---
#### Поддержка партиционирования PostgreSQL из коробкиМапперы для PostgreSQL имеют готовый компонент управлениями партициями (если это настроено для маппера) таблицы которую они обслуживают.
Пример создания партиции и записи в неё :
```csharp
var mappers = ServiceProviderHolder.Instance.GetRequiredService();
var mapper = (MapperObject_A)mappers.GetMapper();var partitionId = 67;
// Создание партиции таблицы.
using (var mappersSession = mappers.OpenSession())
{
mapper.Partitions.CreatePartition(mappersSession, partitionId, partitionId + 1);mappersSession.Commit();
}using (var mappersSession = mappers.OpenSession())
{
// Запись в партицию таблицы.
mapper.New(
mappersSession,
new Object_ADtoNew
{
Id = ComplexIdentity.Build(mapper.Partitions.Level, partitionId, 1),
Value_DateTime = DateTime.Now,
Value_DateTime_NotUpdate = DateTime.Now,
Value_Int = null,
Value_Long = 314,
Value_String = "Text",
});mappersSession.Commit();
}
```