Это приложение демонстрирует, как выполнять атомарные операции создания, обновления и удаления в типичных таблицах «Клиент» и «Транзакция клиента» в отношении «один ко многим», используя Dapper micro-ORM и FluentMap, а также шаблон «РепозиторийЕдиница работы». База данных состоит из файлов DBF, доступ к которым осуществляется с помощью драйвера Visual FoxPro OleDb. На уровне базы данных не реализовано никаких правил, триггеров и т.п.
В качестве примеров использования в проекте «Тесты» представлены некоторые интеграционные тесты с использованием XUnit, а в проекте «SimpleExample» также есть простое консольное приложение.
Это основано на подходе, подробно описанном Яном Руфусом в этой записи в блоге.
Важный! Для этого приложения требуется поставщик OleDb Microsoft Visual FoxPro 9.0. Это только 32-битный провайдер, 64-битной версии нет. В результате это приложение необходимо скомпилировать только для x86.
Dapper — это популярный микро-ORM-сопоставитель объектов, который расширяет IDbConnection удобными методами, которые возвращают результаты базы данных, сопоставленные с типами сущностей.
Вот таблица базы данных в формате DBF:
Field Field Name Type Width
1 CU_CODE Character 10
2 CU_NAME Character 50
3 CU_ADDR1 Character 50
4 CU_ADDR2 Character 50
5 CU_POSTCODE Character 10
6 CU_BALANCE Numeric 12
и вот представляющая его сущность C#.
public class Customer
{
[ Key ]
public string Code { get ; set ; } = string . Empty ;
public string Name { get ; set ; } = string . Empty ;
public string Address1 { get ; set ; } = string . Empty ;
public string ? Address2 { get ; set ; } = string . Empty ;
public string ? Postcode { get ; set ; }
///
/// Gets or sets the customer balance. Not writable. It can only
/// be updated by inserting, deleting or updating a
/// transaction or transactions for this customer.
///
[ Write ( false ) ]
public float Balance { get ; set ; }
public override string ToString ( )
{
return $ " { Code } { Name } " ;
}
}
Затем Dapper предоставляет возможность делать такие вещи, как:
public Customer GetByCode ( string code )
{
var cmd = @"select cu_code, cu_name, cu_addr1, cu_addr2, cu_postcode, cu_balance " ;
cmd += "from Customers where cu_code = ?" ;
return _connection . QueryFirstOrDefault < Customer > ( cmd , param : new { c = code } , transaction ) ;
}
Обратите внимание на способ реализации параметров запроса: OleDB не поддерживает именованные параметры, а только позиционные параметры. Поэтому при использовании нескольких параметров порядок имеет решающее значение:
public void Update ( Customer customer )
{
var cmd = @"update Customers set cu_name=?, cu_addr1=?, cu_addr2=?, cu_postcode=? where cu_code=?" ;
_connection . ExecuteScalar ( cmd , param : new
{
n = customer . Name ,
add1 = customer . Address1 ,
add2 = customer . Address2 ,
pc = customer . Postcode ,
acc = customer . Code ,
} ,
transaction ) ;
}
FluentMap — это расширение Dapper, позволяющее явно объявлять сопоставление между свойствами объекта C# и соответствующими полями таблицы базы данных.
public class CustomerEntityMap : EntityMap < Customer >
{
public CustomerEntityMap ( )
{
Map ( c => c . Code ) . ToColumn ( "cu_code" , caseSensitive : false ) ;
Map ( c => c . Name ) . ToColumn ( "cu_name" , caseSensitive : false ) ;
Map ( c => c . Address1 ) . ToColumn ( "cu_addr1" , caseSensitive : false ) ;
Map ( c => c . Address2 ) . ToColumn ( "cu_addr2" , caseSensitive : false ) ;
Map ( c => c . Postcode ) . ToColumn ( "cu_postcode" , caseSensitive : false ) ;
Map ( c => c . Balance ) . ToColumn ( "cu_balance" , caseSensitive : false ) ;
}
}
Шаблон «Единица работы» позволяет выполнять или откатывать операции создания, обновления и удаления базы данных как одну транзакцию, обеспечивая «атомарность» базы данных, при которой происходят все обновления или не происходит ни одного.
Шаблон репозитория изолирует операции с базой данных от пользовательского интерфейса и позволяет выполнять операции с базой данных путем добавления, обновления или удаления элементов из коллекции объектов.
В приложении есть два класса репозитория: CustomerRepositoty
и CustomerTransactionRepository
. Каждому через конструктор передается параметр типа IDbConnection . Затем из этого параметра извлекается используемое соединение с базой данных:
private IDbConnection _connection { get => databaseTransaction . Connection ! ; }
Обратите внимание на не допускающий нулевых значений '!' оператор. Разумеется, такое внедрение зависимостей делает поставщика базы данных классов независимым. Затем класс содержит различные методы для операций CRUD базы данных, например следующий метод, который возвращает список объектов Customer:
public List < Customer > GetAll ( )
{
var cmd = @"select cu_code, cu_name, cu_addr1, cu_addr2, cu_postcode, cu_balance " ;
cmd += "from Customers " ;
return _connection . Query < Customer > ( cmd , transaction : transaction ) . ToList ( ) ;
}
В этом приложении единица работы представлена классом DapperUnitOfWork
. Это класс, реализующий IDisposable . Он имеет экземпляры обоих типов репозитория. Конструктор принимает строку подключения в качестве параметра и настраивает сопоставление FluentMap, если оно еще не было выполнено. Затем он открывает соединение OleDb и начинает новую транзакцию.
public class DapperUnitOfWork : IDisposable
{
private readonly IDbConnection databaseConnection ;
private IDbTransaction databaseTransaction ;
///
/// Initializes a new instance of the class.
/// Sets up the unit of work and configures the FluentMap mappings.
/// Opens the OleDb connection.
///
/// The OleDb connection string.
public DapperUnitOfWork ( string connString )
{
ConnectionString = connString ;
if ( ! FluentMapper . EntityMaps . Any ( m => m . Key == typeof ( Entities . Customer ) ) )
{
FluentMapper . Initialize ( config =>
{
config . AddMap ( new CustomerEntityMap ( ) ) ;
config . AddMap ( new CustomerTransactionEntityMap ( ) ) ;
} ) ;
}
databaseConnection = new OleDbConnection ( ConnectionString ) ;
databaseConnection . Open ( ) ;
// Some default setup items for the connection.
// 'Set null off' - any inserts will insert the relevant empty value for the database field type instead of a null
// where a value is not supplied.
// 'set exclusive off' - tables will be opened in shared mode.
// 'set deleted on' - unintuitively this means that table rows marked as deleted will be ignored in SELECTs.
var cmd = $ "set null off { Environment . NewLine } set exclusive off { Environment . NewLine } set deleted on { Environment . NewLine } " ;
databaseConnection . Execute ( cmd ) ;
databaseTransaction = databaseConnection . BeginTransaction ( ) ;
}
Свойства репозитория в классе получают транзакцию базы данных, введенную в их геттер, код ниже либо вернет существующий репозиторий, либо создаст новый по мере необходимости:
public CustomerRepository ? CustomerRepository
{
get
{
return customerRepository ??= new CustomerRepository ( dbTransaction ) ;
}
}
Метод Commit()
класса пытается зафиксировать текущую транзакцию. Любое исключение вызовет откат, и исключение будет выброшено. Существует также метод Rollback()
, который можно использовать для явного отката транзакции. В любом случае текущая транзакция будет удалена и создана новая, а члены репозитория будут сброшены.
public void Commit ( )
{
try
{
databaseTransaction . Commit ( ) ;
}
catch
{
databaseTransaction . Rollback ( ) ;
throw ;
}
finally
{
databaseTransaction . Dispose ( ) ;
databaseTransaction = databaseConnection . BeginTransaction ( ) ;
ResetRepositories ( ) ;
}
}
Поскольку объекты репозитория Customer
и CustomerTransaction
используют одну и ту же транзакцию, фиксация или откат являются атомарными и представляют собой одну единицу работы.
Оба метода Commit()
и Rollback()
будут явно вызывать метод Dispose()
класса. Этот метод заботится об удалении текущей транзакции и соединения, а также о сбросе членов репозитория.
public void Dispose ( )
{
dbTransaction ? . Dispose ( ) ;
dbConnection ? . Dispose ( ) ;
GC . SuppressFinalize ( this ) ;
}
Важно. Всегда удалять транзакцию и соединение по завершении чрезвычайно важно в файловой базе данных, такой как DBF. Любые дескрипторы файлов, оставленные открытыми в файле на диске, могут вызвать проблемы для других приложений./
Это простой пример: класс единицы работы здесь является своего рода «объектом-божеством», поскольку он всегда должен содержать экземпляр каждого типа класса репозитория. Так что это кандидат на дальнейшую абстракцию.