Repository 倉儲,你的歸宿究竟在哪?(三)-SELECT 某某某。。。
寫在前面
首先,本篇博文主要包含兩個主題:
- 領域服務中使用倉儲
- SELECT 某某某(有點暈?請看下面。)
上一篇:Repository 倉儲,你的歸宿究竟在哪?(二)-這樣的應用層程式碼,你能接受嗎?
關於倉儲這個系列,很多園友問我:為什麼糾結倉儲?我覺得需要再次說明下(請不要再“糾結”了),引用上一篇博文中某一段評論的回覆:
關於“糾結於倉儲”這個問題,其實博文中我就有說明,不是說我糾結或是陷入這個問題,而是我覺得在實踐領域驅動設計中,倉儲的呼叫是一個很重要的東西,如果使用的不恰當,也許就像上面我所貼出來的應用層程式碼一樣,我個人覺得,這是很多人在實踐領域驅動設計中,很容易踩的一個坑,我只是希望可以把這個過程分享出來,給有相同困惑的人,可以借鑑一下。
領域服務和倉儲的兩種“微妙關係”
這邊的“領域服務”和倉儲的關係,可以理解為在領域中呼叫倉儲,具體表現為在領域服務中使用。
在很久之前,我為了保持所謂的“領域純潔”,在領域服務設計的時候,沒有參雜倉儲任何的呼叫,但是隨著應用程式的複雜,很多業務新增進來,一個單純的“業務描述”並不能真正去實現業務用例,所以這時候的領域服務就被“架空”了,一些業務實現“迫不得已”放在了應用層,也就是上一篇我所貼出的應用層程式碼,不知道你能不能接受?反正我是接受不了,所以我做了一些優化,領域服務中呼叫了倉儲。
關於領域服務中呼叫倉儲,在上一篇博文討論中(czcz1024、Jesse Liu、netfocus、劉標才...),主要得出兩種實現方式,這邊我再大致總結下:
- 傳統方式:倉儲介面定義在領域層,實現在基礎層,通過規約來約束查詢,一般返回型別為聚合根集合物件,如果領域物件的查詢邏輯比較多,具體體現就是倉儲介面變多。
- IQueryable 方式:和上面不同的是介面的設計變少了,因為返回型別為 IQueryable,具體查詢表示式的組合放在了呼叫層,也就是領域服務中,比如:xxxRepository.GetAll().Where(x=>....)
其實這兩種方式都是一把雙刃劍,關鍵在於自己根據具體的業務場景進行選擇了,我說一下我的一些理解,比如現實生活中車庫的場景,我們可以把車庫看作是倉儲,取車的過程看作是倉儲的呼叫,車子的擺放根據汽車的規格,也就是倉儲中的規約概念,比如我今天要開一輛德系、紅色、敞篷、雙門的跑車(條件有點多哈),然後我就去車庫取車,在車庫的“排程系統“(在倉儲的具體表現,可以看作是 EF)中輸入這些命令,然後一輛蘭博基尼就出現在我的眼前了。
在上面描述的現實場景中,如果是第一種傳統方式,“我要開一輛德系、紅色、敞篷、雙門的跑車”這個就可以設計為倉儲的一個介面,為什麼?因為車庫可以換掉,而這些業務用例一般不會進行更改,車庫中的“排程系統”根據命令是如何尋找汽車的呢?答案是規格的組合,也就是倉儲中規約的組合,我們在針對具體業務場景設計的時候,一般會提煉出這個業務場景中的規約,這個也是不可變的,根據命令來進行對這些規約的組合,這個過車的具體體現就是倉儲的實現,約束的是聚合根物件。這種方式中,我個人認為好處是可以充分利用規約,倉儲的具體呼叫統一管理,讓呼叫者感覺不到它是如何工作的,因為它只需要傳一個命令過去,就可以得到想要的結果,唯一不好的地方就是:我心情不好,每天開的汽車都不一樣,這個就要死人了,因為我要設計不同的倉儲介面來進行對規約的組合。
如果是第二種方式,也就是把“排程系統”的使用權交到自己手裡(第一種的這個過程可以看作是通過祕書),這種方式的好與壞,我就不多說了,我現在使用的是第一種方式,主要有兩個原因:
- 防止 IQueryable 的濫用(領域服務非常像 DAL)。
- 現在應用場景中的查詢比較少,沒必要。
上一篇博文中貼出的是,傳送短訊息的應用層程式碼,傳送的業務驗證放在了應用層,以致於 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’ to ‘System.Collections.Generic.IEnumerable’#1>
- Cannot implicitly convert type 'System.Collections.Generic.List' to 'System.Collections.Generic.IEnumerable#1>
- 使用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 指定欄位查詢。
寫在最後
本文轉自田園裡的蟋蟀部落格園部落格,原文連結:http://www.cnblogs.com/xishuai/p/3947897.html,如需轉載請自行聯絡原作者
相關文章
- FreeSql.Repository (一)什麼是倉儲SQL
- 前端的歸宿與價值前端
- [解密] DNA儲存技術究竟牛在哪裡?解密
- 資料倉儲資料中臺區別在哪?
- 大資料時代,資料倉儲究竟是幹嘛的?大資料
- DDD領域驅動設計初探(3):倉儲Repository(下)
- DDD領域驅動設計初探(2):倉儲Repository(上)
- NewSQL 究竟新在哪裡?SQL
- 蘋果的現金儲備究竟花在哪些地方?–資料資訊圖蘋果
- 資料庫和資料倉儲的區別在哪兒?CN資料庫
- .NET ORM 倉儲層必備的功能介紹之 FreeSql Repository 實現篇ORMSQL
- 北京某某某某科技前端開發面試前端面試
- 細說程式設計師最後歸宿程式設計師
- 關於DDD的Repository倉庫需要哪些方法?
- 有了資料湖,資料倉儲究竟能不能被取代?
- 資料倉儲中的三種資料庫模型資料庫模型
- 資料倉儲分層你清楚了嗎
- 設計資料倉儲和資料倉儲的粒度
- 智慧醫院趨勢下,新一代倉儲、物流究竟該如何建設?
- Git 系列(三):建立你的第一個 Git 倉庫Git
- 資料倉儲中的分析SQL——資料倉儲手冊SQL
- 程式設計師的差距在哪裡?程式設計師的三個級別,你在哪裡?程式設計師
- 如何在laravel中使用Repository Pattern(倉庫模式)Laravel模式
- 商業智慧的三個層次,你在哪個層次?
- 輕量雲伺服器才是跨境電商最終歸宿伺服器
- 資料倉儲—資料倉儲—Sybase IQ 介紹
- 網站資料分析:資料倉儲相關的問題(三)網站
- 資料倉儲
- 實現倉儲
- 月薪2000到1萬的運維,究竟差在哪?運維
- 資料倉儲的組成
- 資料倉儲中的概念
- 智慧 | 你真的瞭解自動化倉儲系統嗎?
- 昆明邦芒倉儲物流外包 專業的第三方電商倉配物流服務商
- iThoughtsX for mac思維導圖工具究竟好在哪裡?Mac
- 軟體工程——顆粒歸倉軟體工程
- 你的時間在哪裡 你的成就就在哪裡 評《黑客與畫家》黑客
- [數倉]資料倉儲設計方案