Dapper 做為一個 .Net 的 ORM Library 用簡單來形容他真是太適合不過了。
幾年前在寫 .Net 的程式時習慣用 Entity Framework & Unit of Work 的方式來包裝資料層,剛開始改用 Dapper 腦筋還有些轉不過來。但試了一下後發現,如果把效能當作第一優先考量的話(這也是我現在手上在規劃的專案的優先考量點),Dapper真的是有無法抗拒的魅力。
Dapper 所帶來的優點這邊就不贅述了,這邊要談的是,使用 Dapper 做資料底層的時候,我想到的一些事:
- Unit Of Work (UOW) 怎麼做呢?
- 要怎麼做可單元測試的資料底層?
- 使用時所思考過的事
Unit of Work?
原本使用 Entity Framework 的習慣是建立一個 MyDbContext 物件去繼承 DbContext 物件,然後在裡面定義
public virtual IDbSet { get; set; }
等等屬性代表 DB 裡的資料(表)。
用 Dapper 後,因為直接使用 SqlConnection
連線,連線管理相對變得單純,在網路上拜讀了 Joe Sauve 的一篇文章 Async Dapper with async SQL connection management。發現他所提供的作法非常的乾淨簡潔,照著他的建議實做了自己的程式:
這邊做了一個抽象的 Base Repository `RepositoryBase` 做出包含處理連線資料庫的 Method。
public abstract class RepositoryBase { protected DbConnectionFactory DbConnectionFactory; protected RepoBase(DbConnectionFactory connectionFactory) { DbConnectionFactory = connectionFactory; } /// /// 這 Method 就是 Joe 所建議的 Async 模式,詳情可以參考 Joe 的 Blog /// [Async Dapper with async SQL connection management](http://www.joesauve.com/async-dapper-and-async-sql-connection-management/) /// protected async Task WithConnectionAsync(Func dbOperation) { try { using (var connection = DbConnectionFactory.GetSqlDbConnection()) { await connection.OpenAsync(); // Asynchronously open a connection to the database return await dbOperation(connection); // Asynchronously execute getData, which has been passed in as a Func } } catch (TimeoutException ex) { throw new Exception(String.Format( "{0}.WithConnectionAsync() experienced a SQL timeout", GetType().FullName), ex); } catch (SqlException ex) { throw new Exception(String.Format( "{0}.WithConnectionAsync() experienced a SQL exception (not a timeout)", GetType().FullName), ex); } } }
Joe 的這個做法,漂亮的地方在於;這樣的做法簡單的把 開啟連線,Try/Catch 都集中在一個地方處理。實做這個 abstract class 時只需要把需要執行的 .Execute() .Query() 包在 `async connection => { } ` 裡就好。
更棒的是這個方式同樣照顧到需要寫非同步處理的需要!
下面是一個繼承實做 RepositoryBase 的 class `CustomerRepository`。
public class CustomerRepository : RepositoryBase, ICustomerRepository { public CustomerRepository(DbConnectionFactory conn) : base(conn) { } // .... 與 Customer 有關的 Method 可放在這裡, // 比如說 CreateCustomer(), CreateAddress() // 這些新增可能都會跨好幾個表格 // 像這個新增 Customer async Task ICustomerRepository.CreateCustomerAsync(Customer customer) { var customerId = Guid.NewGuid(); return await WithConnectionAsync(async c => { using(var t = t.BeginTransaction()){ // 建立 Customer 資料 c.Execute(@"INSERT INTO Customers (Id, Name) VALUES(@Id, @CustomerName)", new { Id = customerId AddressText = customer.name }, transaction: t); // 住址。這邊假設 Customer 關連到多筆住址 c.Execute(@"INSERT INTO Addresses (Id, Name) VALUES(@AddressId, @AddressText)", new { AddressId = Guid.NewGuid(), AddressText = customer.addressText, .... CustomerId = customerId }, transaction: t); // 送 DB 執行 t.Commit(); return customerId; } }); } }
單元測試
單元測試是在用了 Dapper 的時候覺得很不方便的事情。當 TSQL 都需要寫在程式或 Stored Procedure 裡,單元測試就不得不做一些變更了。
用 EF 可以輕鬆的 Mock 測試資料塞到 Service Class 裡來驗證商務邏輯,但 Dapper 只能真的準備一個資料庫(In Memory 或 localdb 之類的)來做類似整合測試來涵蓋底層資料存取的邏輯。
比起直接建一個 ICollection 當作測試資料當然是慢了點。而且並不能完整符合的單元測試的定義。但就要看使用 Dapper 要達成的主要目的的價值是否是我們要的。
使用時所思考過的事
目前的資料庫是與 DBA 討論而一起建立起來而不像之前由 EF Code First 建起來的。我在看 Repository 就比較像是 Aggregate Root 的樣子,把一些相關的底層表格整理成一個 Repository。
簡單的來說,我在做底層的時候,比較需要在意的事情應該是 Domain Model 的設計。既然 Repository 被當成 Aggregate Root。以我現在正在想辦法實現的架構 (CQRS):
Command 可能較為單純一些,把要對資料異動的幾個動作一起進去即可,但 Query 的 Method 可能就會包含整合不同 Aggregate Root 的一些 Projection。
但目前仍然在想說,是不是有需要把 Query 跟 Command 的 Method 拆開來寫在不同的 Repository。
設計決定有時候真的很難 …
稍微結論一下,本篇討論了一下使用 Dapper 來做底層存取時我自己遇到與想到的一些問題:單元測試、非同步方法的實做等等。
手上在設計的系統會有很大流量。之前在做 CQRS 使用的是 EF 來打底,效能上沒有遇到太多 Application 造成的效能瓶頸,但 Dapper 應該還可以再讓系統跑快一點。