Repository 倉儲,你的歸宿究竟在哪?(三)-SELECT 某某某。。。

weixin_33766168發表於2017-11-15

寫在前面

首先,本篇博文主要包含兩個主題:

  1. 領域服務中使用倉儲
  2. SELECT 某某某(有點暈?請看下面。)

上一篇:Repository 倉儲,你的歸宿究竟在哪?(二)-這樣的應用層程式碼,你能接受嗎?

關於倉儲這個系列,很多園友問我:為什麼糾結倉儲?我覺得需要再次說明下(請不要再“糾結”了),引用上一篇博文中某一段評論的回覆:

關於“糾結於倉儲”這個問題,其實博文中我就有說明,不是說我糾結或是陷入這個問題,而是我覺得在實踐領域驅動設計中,倉儲的呼叫是一個很重要的東西,如果使用的不恰當,也許就像上面我所貼出來的應用層程式碼一樣,我個人覺得,這是很多人在實踐領域驅動設計中,很容易踩的一個坑,我只是希望可以把這個過程分享出來,給有相同困惑的人,可以借鑑一下。

領域服務和倉儲的兩種“微妙關係”

這邊的“領域服務”和倉儲的關係,可以理解為在領域中呼叫倉儲,具體表現為在領域服務中使用。

在很久之前,我為了保持所謂的“領域純潔”,在領域服務設計的時候,沒有參雜倉儲任何的呼叫,但是隨著應用程式的複雜,很多業務新增進來,一個單純的“業務描述”並不能真正去實現業務用例,所以這時候的領域服務就被“架空”了,一些業務實現“迫不得已”放在了應用層,也就是上一篇我所貼出的應用層程式碼,不知道你能不能接受?反正我是接受不了,所以我做了一些優化,領域服務中呼叫了倉儲。

關於領域服務中呼叫倉儲,在上一篇博文討論中(czcz1024、Jesse Liu、netfocus、劉標才...),主要得出兩種實現方式,這邊我再大致總結下:

  1. 傳統方式:倉儲介面定義在領域層,實現在基礎層,通過規約來約束查詢,一般返回型別為聚合根集合物件,如果領域物件的查詢邏輯比較多,具體體現就是倉儲介面變多。
  2. IQueryable 方式:和上面不同的是介面的設計變少了,因為返回型別為 IQueryable,具體查詢表示式的組合放在了呼叫層,也就是領域服務中,比如:xxxRepository.GetAll().Where(x=>....)

其實這兩種方式都是一把雙刃劍,關鍵在於自己根據具體的業務場景進行選擇了,我說一下我的一些理解,比如現實生活中車庫的場景,我們可以把車庫看作是倉儲,取車的過程看作是倉儲的呼叫,車子的擺放根據汽車的規格,也就是倉儲中的規約概念,比如我今天要開一輛德系、紅色、敞篷、雙門的跑車(條件有點多哈),然後我就去車庫取車,在車庫的“排程系統“(在倉儲的具體表現,可以看作是 EF)中輸入這些命令,然後一輛蘭博基尼就出現在我的眼前了。

在上面描述的現實場景中,如果是第一種傳統方式,“我要開一輛德系、紅色、敞篷、雙門的跑車”這個就可以設計為倉儲的一個介面,為什麼?因為車庫可以換掉,而這些業務用例一般不會進行更改,車庫中的“排程系統”根據命令是如何尋找汽車的呢?答案是規格的組合,也就是倉儲中規約的組合,我們在針對具體業務場景設計的時候,一般會提煉出這個業務場景中的規約,這個也是不可變的,根據命令來進行對這些規約的組合,這個過車的具體體現就是倉儲的實現,約束的是聚合根物件。這種方式中,我個人認為好處是可以充分利用規約,倉儲的具體呼叫統一管理,讓呼叫者感覺不到它是如何工作的,因為它只需要傳一個命令過去,就可以得到想要的結果,唯一不好的地方就是:我心情不好,每天開的汽車都不一樣,這個就要死人了,因為我要設計不同的倉儲介面來進行對規約的組合。

如果是第二種方式,也就是把“排程系統”的使用權交到自己手裡(第一種的這個過程可以看作是通過祕書),這種方式的好與壞,我就不多說了,我現在使用的是第一種方式,主要有兩個原因:

  1. 防止 IQueryable 的濫用(領域服務非常像 DAL)。
  2. 現在應用場景中的查詢比較少,沒必要。

上一篇博文中貼出的是,傳送短訊息的應用層程式碼,傳送的業務驗證放在了應用層,以致於 SendSiteMessageService.SendMessage 中只有一段“return true”程式碼,修改之後的領域服務程式碼:

    public class SendSiteMessageService : ISendMessageService
    {
        public async Task<bool> SendMessage(Message message)
        {
            IMessageRepository messageRepository = IocContainer.Resolver.Resolve<IMessageRepository>();
            if (message.Type == MessageType.Personal)
            {
                if (System.Web.HttpContext.Current != null)
                {
                    if (await messageRepository.GetMessageCountByIP(Util.GetUserIpAddress()) > 100)
                    {
                        throw new CustomMessageException("一天內只能傳送100條短訊息");
                    }
                }
                if (await messageRepository.GetOutboxCountBySender(message.Sender) > 20)
                {
                    throw new CustomMessageException("1小時內只能向20個不同的使用者傳送短訊息");
                }
            }
            return true;
        }
    }

程式碼就是這樣,如果你覺得有問題,歡迎提出,我再進行修改。

這邊再說一下領域服務中倉儲的注入,緣由是我前幾天看了劉標才的一篇博文:DDD領域驅動設計之領域服務,文中對倉儲的注入方式是通過建構函式,這種方式的壞處就是領域服務對倉儲產生強依賴關係,還有就是如果領域服務中注入了多個倉儲,呼叫這個領域服務中的某一個方法,而這個方法只是使用了一個倉儲,那麼在對這個領域服務進行注入的時候,就必須把所有倉儲都要進行注入,這就沒有必要了。

解決上面的問題的方式就是,在使用倉儲的地方對其進行解析,比如:IocContainer.Resolve<IMessageRepository>();,這樣就可以避免了上面的問題,我們還可以把倉儲的注入放在 Bootstrapper 中,也就是專案啟動的地方。

SELECT 某某某

上面所探討的都是倉儲的呼叫,而現在這個問題是倉儲的實現,這是兩種不同的概念。

什麼是“SELECT 某某某”?答案就是針對欄位進行查詢,場景為應用程式的效能優化。我知道你看到“SELECT”就想到了事務指令碼模式,不要想歪了哦,你眼中的倉儲實現不一定是 ORM,也可以是傳統的 ADO.NET,如果倉儲實現使用的是資料庫持久化機制,其實再高階的 ORM,到最後都會轉換成 SQL 程式碼,具體表現就是對這些程式碼的優化,似乎不屬於領域驅動設計的範疇了,但不可否認,這是應用程式不能不考慮的。

應用程式中的效能問題

我說一下現在短訊息專案中倉儲的實現(常用場景):底層使用的是 EntityFramework,為了更好的理解,我貼一段查詢程式碼:

        protected override async Task<IEnumerable<TAggregateRoot>> FindAll(ISpecification<TAggregateRoot> specification, System.Linq.Expressions.Expression<Func<TAggregateRoot, dynamic>> sortPredicate, SortOrder sortOrder, int pageNumber, int pageSize)
        {
            var query = efContext.Context.Set<TAggregateRoot>()
                .Where(specification.GetExpression());
            int skip = (pageNumber - 1) * pageSize;
            int take = pageSize;

            if (sortPredicate != null)
            {
                switch (sortOrder)
                {
                    case SortOrder.Ascending:
                        return query.SortBy(sortPredicate).Skip(skip).Take(take).ToListAsync();
                    case SortOrder.Descending:
                        return query.SortByDescending(sortPredicate).Skip(skip).Take(take).ToListAsync();
                    default:
                        break;
                }
            }
            return query.Skip(skip).Take(take).ToListAsync();
        }

這種方式有什麼問題嗎?至少在我們做一些 DDD 示例的時候,沒有任何問題,為什麼?因為你沒有實際去應用,也就體會不到一些問題,前一段時間短訊息頁面載入慢,一個是資料庫索引問題(詳見:程式設計師眼中的 SQL Server-執行計劃教會我如何建立索引?),還有一個就是訊息列表查詢的時候,把訊息表的所有欄位都取出來了,這是完全沒有必要的,比如訊息內容就不需要進行讀取,但是我們在跟蹤上面程式碼執行的時候,會發現 EntityFramework 生成的 SQL 程式碼為 SELECT *。。。

走過的彎路

上面這個問題,至少從那個資料庫索引問題解決完,我就一直鬱悶著,也嘗試著用各種方式去解決,比如建立 IQueryable 的 Select 表示式,傳入的是自定義的聚合根屬性,還有就是擴充套件 Select 表示式,詳細過程就不回首了,我貼一下當時在搜尋時的一些資料:

在 EntityFramework 底層,我們 Get 查詢的時候,一般都是返回 TAggregateRoot 聚合根集合物件,也就是說,你沒有辦法在底層進行指定屬性查詢,因為聚合根只有 ID 一個屬性,唯一的辦法就是傳入 Expression<Func<TAggregateRoot, TAggregateRoot>> selector 表示式,select 兩個範型約束為 TSource 和 TDest,這邊我們兩種型別都為 TAggregateRoot ,但是執行結果為:“The entity or complex type ... cannot be constructed in a LINQ to Entities query.”,給我的教訓就是 Select 中的 TSource 和 TDest 不能為同一型別(至少指定屬性的情況下)。

我的解決方案

EntityFramework 底層的所有查詢返回型別改為 IQueryable<TAggregateRoot>,倉儲的查詢返回型別改為 IEnumerable<MessageListDTO>,為什麼是 MessageListDTO 而不是 Message?因為我覺得訊息列表的顯示,就是對訊息的扁平化處理,沒必要是一個 Message 實體物件,雖然它是一個訊息實體倉儲,就好比從車庫中取出一個所有汽車列表的單子,有必要把所有汽車實體取出來嗎?很顯然沒有必要,我們只需要取出汽車的一些資訊即可,我覺得這是應對業務場景變化所必須要調整的,具體的實現程式碼:

        public async Task<IEnumerable<MessageListDTO>> GetInbox(Contact reader, PageQuery pageQuery)
        {
            return await GetAll(new InboxSpecification(reader), sp => sp.ID, SortOrder.Descending, pageQuery.PageIndex, pageQuery.PageSize)
                 .Project().To<MessageListDTO>()
                 .ToListAsync();
        }

“Project().To()” 是什麼東西?這是 AutoMapper 對 IQueryable 表示式的一個擴充套件,詳情請參閱:戀愛雖易,相處不易:當 EntityFramework 愛上 AutoMapper,AutoMapper 擴充套件說明:Queryable Extensions,簡單的一段程式碼就可以完成實體與 DTO 之間的轉化,我們再次用 SQL Server Profiler 捕獲生成的 SQL 程式碼,就會發現,這就是我們想要的,根據對映配置 Select 指定欄位查詢。

寫在最後

本文轉自田園裡的蟋蟀部落格園部落格,原文連結:http://www.cnblogs.com/xishuai/p/3947897.html,如需轉載請自行聯絡原作者

相關文章