天天看點

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

寫在前面

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

  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 表達式,詳細過程就不回首了,我貼一下當時在搜尋時的一些資料:

  • IQueryable C# Select
  • The entity cannot be constructed in a LINQ to Entities query
  • Cannot implicitly convert type 'System.Collections.Generic.IEnumerable' to 'System.Collections.Generic.List'. An explicit conversion exists (are you missing a cast?)
  • Cannot implicitly convert type ‘System.Linq.IQueryable<AnonymousType#1>’ to ‘System.Collections.Generic.IEnumerable’
  • Cannot implicitly convert type 'System.Collections.Generic.List<AnonymousType#1>' to 'System.Collections.Generic.IEnumerable
  • 使用Entity Framework時要注意的一些性能問題
  • 不記得了...

在 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 指定字段查詢。

寫在最後

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

針對“SELECT 某某某”這個實際應用問題,以上隻是我的個人實作方式,如果你有疑問或是有更好的實作,歡迎指教。。。

作者:田園裡的蟋蟀

微信公衆号:你好架構

出處:http://www.cnblogs.com/xishuai/

公衆号會不定時的分享有關架構的方方面面,包含并不局限于:Microservices(微服務)、Service Mesh(服務網格)、DDD/TDD、Spring Cloud、Dubbo、Service Fabric、Linkerd、Envoy、Istio、Conduit、Kubernetes、Docker、MacOS/Linux、Java、.NET Core/ASP.NET Core、Redis、RabbitMQ、MongoDB、GitLab、CI/CD(持續內建/持續部署)、DevOps等等。

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接。

分享到:

QQ空間

新浪微網誌

騰訊微網誌

微信

更多

繼續閱讀