初試 Dapper

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):

domain

Command 可能較為單純一些,把要對資料異動的幾個動作一起進去即可,但 Query 的 Method 可能就會包含整合不同 Aggregate Root 的一些 Projection。

但目前仍然在想說,是不是有需要把 Query 跟 Command 的 Method 拆開來寫在不同的 Repository。

設計決定有時候真的很難 …


稍微結論一下,本篇討論了一下使用 Dapper 來做底層存取時我自己遇到與想到的一些問題:單元測試、非同步方法的實做等等。

手上在設計的系統會有很大流量。之前在做 CQRS 使用的是 EF 來打底,效能上沒有遇到太多 Application 造成的效能瓶頸,但 Dapper 應該還可以再讓系統跑快一點。

References