该应用程序演示了如何使用 Dapper micro-ORM 和 FluentMap 以及 RepositoryUnit Of Work 模式,以一对多关系对典型的 Customer 和 Customer Transaction 表执行原子创建、更新和删除操作。该数据库由 DBF 文件组成,并使用 Visual FoxPro OleDb 驱动程序进行访问。在数据库级别没有实施任何规则、触发器或类似的规则。
通过使用示例,“Tests”项目中提供了一些使用 XUnit 的集成测试,“SimpleExample”项目中还有一个简单的控制台应用程序。
这是基于 Ian Rufus 在此博客文章中详细介绍的方法。
重要的!此应用程序需要 Microsoft Visual FoxPro 9.0 OleDb Provider。这是仅 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 ( ) ;
}
类上的存储库属性将数据库事务注入到其 getter 中,下面的代码将返回现有存储库或根据需要创建一个新存储库:
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)中,始终在完成后处理事务和连接非常重要。磁盘文件上保持打开状态的任何文件句柄都可能导致其他应用程序出现问题。/
这是一个简单的示例 - 这里的工作单元类是一种“上帝对象”,因为它始终必须包含每种类型的存储库类的实例。因此它是进一步抽象的候选者。