應用程式示範如何使用 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)中,始終在完成後處理事務和連接非常重要。磁碟檔案上保持開啟狀態的任何檔案句柄都可能導致其他應用程式出現問題。
這是一個簡單的範例 - 這裡的工作單元類別是一種“上帝物件”,因為它始終必須包含每種類型的儲存庫類別的實例。因此它是進一步抽象的候選者。