OSharp是什么?
OSharp是個快速開發框架,但不是一個大而全的包羅萬象的框架,嚴格的說,OSharp中什么都沒有實現。與其他大而全的框架最大的不同點,就是OSharp只做抽象封裝,不做實現。依賴注入、ORM、對象映射、日志、緩存等等功能,都只定義了一套最基礎最通用的抽象封裝,提供了一套統一的API、約定與規則,並定義了部分執行流程,主要是讓項目在一定的規范下進行開發。所有的功能實現端,都是通過現有的成熟的第三方組件來實現的,除了EntityFramework之外,所有的第三方實現都可以輕松的替換成另一種第三方實現,OSharp框架正是要起隔離作用,保證這種變更不會對業務代碼造成影響,使用統一的API來進行業務實現,解除與第三方實現的耦合,保持業務代碼的規范與穩定。
本文已同步到系列目錄:OSharp快速開發框架解說系列
前言
數據層設計真是一個百說不厭的話題,大系統說並發量,說高性能;小系統追求開發效率,易維護性各有各的追求。
OSharp 開發框架的定位是中小系統, 數據層的開發效率與易用性的權重就比較高了,所以,使用ORM當然是首選。在 .net 環境下,有眾多的閉源的開源的優秀的ORM組件,從各方便對比來看,EntityFramework 是不二之選。一提起 EntityFramework,不少同學又要蠢蠢欲動來吐槽其性能了。其實,經過幾個版本的更新換代,現在的穩定版 EntityFramework 6 已經相當好用了,nuget 上截止到目前 “8,830,918 total downloads” 已經足夠能說明問題了,EntityFramework 在整個 .net 世界是相當受歡迎的。不過,不管哪個技術平台,能不能用好一個技術與技術水平有很大的關系,如果沒追求,隨處的 select * from xxx,傳說中高性能的 ado.net 也不會高到哪去。
為什么強依賴於EntityFramework
在本系列開篇《總體設計》對 OSharp 開發框架的數據存儲組件的介紹時,就強調了 OSharp 將“強依賴”於 EntityFramework 這個數據訪問組件。當時就有人提出了“強依賴Entity framework是個坑吧”的疑問。這里簡要解釋一下為什么 OSharp 不打算做成兼容各種數據訪問方案(EF,NH,ado.net 等等)。我們來看看統一各個ORM需要付出的代價:
- 需要統一標准,業務層與數據層之間的數據交互載體只能是POCO數據實體,如果個別ORM需要對實體有特殊要求(如NH要求實體屬性為virtual),需要在ORM實現內部進行適配轉換。
- 數據交互只能是實體,這就導致了參數數據查詢的所有 sql 語句都是“select * from ...”這種方式,這將給系統性能帶來傷害。
- 要兼容各個ORM,數據層只能提供一些能普遍適應的 API,放棄各種 ORM 的優點與特色(如EF的linq查詢)
基於上面的一些理由,我們發現要兼容各種數據訪問方案,需要付出的代碼是很大的,很不划算。因此,OSharp 將專注於一款 ORM,從各方面比較,EntityFramework 是一個好選擇,理由如下:
- EntityFramework 是微軟大力發展的一個開源項目,EF 6 在 codeplex.com 開源,EF 7 在 github.com 隨 ASP.NET 5 開源。
- EntityFramework 能輕松支持各大主流數據庫,只要引入相應數據庫的 DataProvider 即可,能無差異的操作各大主流數據庫。
- EntityFramework 支持 linq to entities 語句查詢,強類型支持,高效實現查詢需求。
- EntityFramework 全面封裝了數據庫細節,使用了大量的“約定勝於配置”的思想,使開發者不必直接對關系存儲架構編程,減少代碼量,減輕維護工作,並使項目可維護性更高。
為什么要封裝EntityFramework
在計划使用EntityFramework來在項目中實現數據存儲時,遇到的第一個問題就是:怎樣來使用EntityFramework?要不要對EntityFramework進行二次封裝?
反對對 EntityFramework 進行封裝的同學,通常會有如下理由:
- EntityFramework 提供的API已經足夠簡單了,已經有了非常良好的易用性,沒有再封裝的必要。
- EntityFramework 內部已經實現了一個 UnitOfWork + Repository 的封裝,沒有必要再包裝一次。
- EntityFramework 提供的API非常靈活且很有特色,封裝有可能會喪失其靈活性
我認為,如果是小項目,且所有開發成員都能很好的使用 EntityFramework,在業務層中直接使用,將 EntityFramework 的靈活性發揮出來,也是非常好的。但是,直接使用 EntityFramework 的話,也有不少弊端,對於 OSharp 這樣一個開發框架而言,封裝就顯得非常有必要了,原因如下:
- 不是所有的開發人員都對 EntityFramework 足夠熟悉,封裝之后能將 EntityFramework 的細節及較敏感的 API 進行包裝與隱藏,使 EntityFramework 的使用更加透明易用。
- 統一的封裝,有利於對業務層提供統一的 API,對業務層的代碼規范非常有利。
- 封裝有利於業務實體與 EntityFramework 的解耦。如果不封裝,所有業務實體模型(Model)都要在上下文類中設置一個 DbSet<TEntity> 類型的實體集,將與上下文強耦合,當需求發生變化時,都要對原有代碼進行修改,很不利於維護。而封裝之后,所有的實體模型都是動態加載到上下文類中的,業務實體與 EntityFramework 能夠完全解耦,大大增強系統的可維護性。
EntityFramework封裝的常見誤區
在 EntityFramework 的發展過程中,很多使用者都在抱怨 EntityFramework 性能低下,其實很多時候都是因為對 EntityFramework 沒有足夠的了解,走進了誤區所致。那么,EntityFramework 會有哪些誤區呢?這里我列幾個我所了解的“坑”,歡迎補充。
錯誤使用返回類型,不了解 IEnumerable<T> 與 IQueryable<T> 的區別
在設計數據訪問層的查詢API的時候,IEnumerable<T> 和 IQueryable<T> 都可以作為集合類查詢結果的返回類型,那么,這兩者有什么區別呢?為什么誤用的時候會造成致命的性能問題呢?
IEnumerable<T> 接口的聲明為:
1 /// <summary> 2 /// 公開枚舉數,該枚舉數支持在指定類型的集合上進行簡單迭代。 3 /// </summary> 4 public interface IEnumerable<out T> : IEnumerable
IQueryable<T> 接口的聲明為:
1 /// <summary> 2 /// 提供對數據類型已知的特定數據源的查詢進行計算的功能。 3 /// </summary> 4 public interface IQueryable<out T> : IEnumerable<T>, IQueryable, IEnumerable
在進行查詢的時候,IEnumerable<T> 接口接受一個 Func<T, bool> 類型的委托參數: public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate); ,而 IQueryable<T> 接口接受一個 Expression<Func<T, bool>> 類型的表達式參數: public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate); 。
正因為 IEnumerable<T> 接受的參數 predicate 數據類型是委托類型,所以這個參數在被調用的時候,就會立即執行查詢邏輯,然后將查詢的結果保存在內存中,后續的查詢邏輯是完全在內存中執行的。而 IQueryable<T> 接受的參數 predicate 數據類型是表達式類型,這個參數會一直往下傳遞,直到被 IQueryable 中的 IQueryableProvider 類型的 Provider 屬性解析成真正的查詢語句(如 sql 語句),才傳到數據源中進行查詢動作。
所以,如果查詢返回的數據集合很大的時候,使用 IEnumerable<T> 作為返回類型,會將這個數據集合立即加載到內存中,比如在設計 IRepository<T> 的 API 時,設計 IEnumerable<T> GetAll(); , IEnumerable<T> GetByPredicate(Func<T, bool> predicate); 這種 API ,是非常可怕的,如果一個表中有幾十上百萬的數據,也同樣會把所有數據加載到內存中,可能直接就導致服務器宕機了。即使數據量不大,當並發量上來的時候,也同樣會造成極大的性能問題。
過早地內存化數據
IEnumerable<T> 類型與 IQueryable<T> 類型是支持延遲的,沒有真正使用數據之前,不管怎么調用,都不會執行查詢,數據還是在數據庫內,只有真正使用數據的時候,才會執行查詢,把數據本地化到內存中。這樣一來,什么時候執行本地化操作(ToArray(),ToList()等操作)就顯得非常重要了,如果過早的執行本地化操作,那么就容易造成加載到內存的數據集合過於龐大,記錄條數過多,造成性能問題。因此,在進行數據查詢的時候,原則上應該按需獲取數據,取出的數據集合就盡量的小,字段應盡量少,到數據真正使用的時候,才執行數據內存本地化操作。對於篩選部分字段的需求,linq to entities 的 select 查詢匿名結果的查詢方式,提供了有力的支持。
導航屬性的延遲加載與循環
EntityFramework 實體模型的導航屬性(即與當前表有外鍵關系的關聯表)通常標記為 virtual,標記為 virtual 之后,相應屬性的數據是具有延遲加載的特性的,只有真正用到相應屬性的數據時,才會根據外鍵關系執行相應的查詢動作,加載相應的數據。延遲加載的特性,能給系統性能帶來優化,因為加載主干實體時只加載主干實體的信息,不會把關聯實體的信息都加載進來,關聯實體的數據只有用到的時候都會去加載。但也正是因為延遲加載,導航屬性的數據是用到一次就執行一次查詢動作,加載一次數據,一次還如,如果對於相同實體,需要多次用到同一個導航屬性,就會產生多次重復的查詢動作來加載導航屬性的數據,給系統帶來性能問題。例如如下的操作:
正確的做法,當需要在循環中使用導航屬性時,應在循環之前加載主干實體數據時,把導航屬性的數據使用 Include 查詢一起加載:
數據查詢方式
EntityFramework 給我們提供了直接使用 實體對象 的眾多查詢 API,如果我們在實現業務的時候,使用直接操作 實體對象 的方式,同樣也會給系統造成性能問題。因為 EntityFramework 每執行一次查詢,都會將實體的所有字段取出來,等同於每次都執行“ select * from xxx ” 的查詢語句。更可怕的是導航屬性的使用,因為導航屬性的數據類型通常都定義為 ICollection<T> 或者 ICollection<T> 的派生類型,ICollection<T> 類型是什么?內存集合類型!也就是說,當我們直接調用 entity.NavigationProperties 這樣一個導航屬性的時候,會將 NavigationProperty 這個關聯表的“所有數據”都加載到內存中。想想,如果這個關聯表的數據幾十上百萬的話,那將是多么可怕的性能災難!!
那么,怎樣編寫數據查詢的代碼呢,個人認為應該分析場景:
- 如果查詢出來的數據需要進行更新,刪除等操作,就使用實體查詢的方式。
- 如果查詢出來的數據不需要再存回到數據庫中,只是為了業務判斷使用,則是按需要使用 Select 進行匿名對象查詢的方式來只查詢出必要的實體屬性。
PS:關於 EntityFramework 數據查詢的更多內容,歡迎閱讀 架構系列的《數據查詢》篇。
怎樣設計 EntityFramework 數據層
前面說了那么多誤區,那怎樣來設計 EntityFramework 的數據訪問層 API呢?這個話題在前面的 架構系列 中也討論過了,但為了全面了解 OSharp 開發框架,這里不免要添點新料再炒次舊飯。
業務實體模型基類 EntityBase
為了能在底層對所有的實體模型類進行統一管理,並規范實體類必要的屬性設定,定義了一個如下的 實體模型基類 EntityBase<TKey>。為適應不同主鍵數據類型的需求,定義了一個泛型 Id 類型,在各個實際實體模型中可以設置實體的主鍵數據類型。
1 /// <summary> 2 /// 可持久化到數據庫的數據模型基類 3 /// </summary> 4 /// <typeparam name="TKey"></typeparam> 5 public abstract class EntityBase<TKey> 6 { 7 protected EntityBase() 8 { 9 IsDeleted = false; 10 CreatedTime = DateTime.Now; 11 } 12 13 #region 屬性 14 15 /// <summary> 16 /// 獲取或設置 實體唯一標識,主鍵 17 /// </summary> 18 [Key] 19 public TKey Id { get; set; } 20 21 /// <summary> 22 /// 獲取或設置 是否刪除,邏輯上的刪除,非物品刪除 23 /// </summary> 24 public bool IsDeleted { get; set; } 25 26 /// <summary> 27 /// 獲取或設置 創建時間 28 /// </summary> 29 public DateTime CreatedTime { get; set; } 30 31 /// <summary> 32 /// 獲取或設置 版本控制標識,用於處理並發 33 /// </summary> 34 [ConcurrencyCheck] 35 [Timestamp] 36 public byte[] Timestamp { get; set; } 37 38 #endregion 39 40 #region 方法 41 42 /// <summary> 43 /// 判斷兩個實體是否是同一數據記錄的實體 44 /// </summary> 45 /// <param name="obj">要比較的實體信息</param> 46 /// <returns></returns> 47 public override bool Equals(object obj) 48 { 49 if (obj == null) 50 { 51 return false; 52 } 53 EntityBase<TKey> entity = obj as EntityBase<TKey>; 54 if (entity == null) 55 { 56 return false; 57 } 58 return Id.Equals(entity.Id) && CreatedTime.Equals(entity.CreatedTime); 59 } 60 61 /// <summary> 62 /// 用作特定類型的哈希函數。 63 /// </summary> 64 /// <returns> 65 /// 當前 <see cref="T:System.Object"/> 的哈希代碼。 66 /// </returns> 67 public override int GetHashCode() 68 { 69 return Id.GetHashCode() ^ CreatedTime.GetHashCode(); 70 } 71 72 #endregion 73 }
業務單元操作接口 IUnitOfWork
相比 架構系列 的 數據存儲上下文管理,OSharp 的存儲上下文進行了簡化,OSharp 中的 IUnitOfWork 接口將直接作為 上下文類(DbContext的派生類)的一個接口而存在。其中定義了一個默認關閉的 TransactionEnabled 屬性開關對“是否開啟事務提交”進行管理。在默認的狀態下,事務操作是關閉的(與 EntityFramework 的默認開啟相反),調用 IRepository 對實體進行 增、改、刪 操作,立即向數據庫提交操作,這在進行單步操作的時候,很方便,不用每次都去調用一次 SaveChanges() 操作進行提交。在需要進行事務操作(同時提交多步操作,成功一起成功,失敗一起失敗)時,則需要將 TransactionEnabled 設置為 true,當調用 IRepository 對實體進行 增、改、刪 操作時,會申請更改,但不會向數據庫提交操作,需要在最后手動去調用 UnitOfWork.SaveChanges() 操作進行提交。
1 /// <summary> 2 /// 業務單元操作接口 3 /// </summary> 4 public interface IUnitOfWork : IDependency 5 { 6 #region 屬性 7 8 /// <summary> 9 /// 獲取或設置 是否開啟事務提交 10 /// </summary> 11 bool TransactionEnabled { get; set; } 12 13 #endregion 14 15 #region 方法 16 17 /// <summary> 18 /// 提交當前單元操作的更改。 19 /// </summary> 20 /// <returns>操作影響的行數</returns> 21 int SaveChanges(); 22 23 #if NET45 24 25 /// <summary> 26 /// 異步提交當前單元操作的更改。 27 /// </summary> 28 /// <returns>操作影響的行數</returns> 29 Task<int> SaveChangesAsync(); 30 31 #endif 32 33 #endregion 34 }
實體倉儲數據操作接口 IRepository
實體倉儲數據操作接口 IRepository 的 API,是整個數據層設計的核心。IRepository 接口被定義為“實體類型、主鍵類型”的雙泛型接口,實體類型限定為前面定義的實體基類 EntityBase<TKey> 的派生類,聲明如下:
1 interface IRepository<TEntity, TKey> : IDependency where TEntity : EntityBase<TKey>
IRepository 中定義了兩個屬性:
- IUnitOfWork 類型的單元操作對象 UnitOfWork,此接口主要用於業務層執行事務操作。
- IQueryable<TEntity> 類型的 Entities,作為向業務層開放的 TEntity 實體類型的 查詢數據源。OSharp 的大部分數據查詢,均通過定義 IQueryable<T> 類型的擴展方法來完成。定義為 IQueryable<TEntity> 類型,即能保證 EntityFramework 組件原有的查詢自由度,又能阻止業務層使用 此數據源 進行 增、改、刪 等業務操作,必須調用 IRepository 中規定的 API 才能完成實體的業務操作。
在操作 API 上,IRepository 接口主要定義了如下幾種 API:
普通 增、改、刪 業務操作 API
普通業務操作API主要是對單個或多個實體進行的單個或批量操作API:
1 /// <summary> 2 /// 插入實體 3 /// </summary> 4 /// <param name="entity">實體對象</param> 5 /// <returns>操作影響的行數</returns> 6 int Insert(TEntity entity); 7 8 /// <summary> 9 /// 批量插入實體 10 /// </summary> 11 /// <param name="entities">實體對象集合</param> 12 /// <returns>操作影響的行數</returns> 13 int Insert(IEnumerable<TEntity> entities); 14 15 /// <summary> 16 /// 更新實體對象 17 /// </summary> 18 /// <param name="entity">更新后的實體對象</param> 19 /// <returns>操作影響的行數</returns> 20 int Update(TEntity entity); 21 22 /// <summary> 23 /// 刪除實體 24 /// </summary> 25 /// <param name="entity">實體對象</param> 26 /// <returns>操作影響的行數</returns> 27 int Delete(TEntity entity); 28 29 /// <summary> 30 /// 刪除指定編號的實體 31 /// </summary> 32 /// <param name="key">實體編號</param> 33 /// <returns>操作影響的行數</returns> 34 int Delete(TKey key); 35 36 /// <summary> 37 /// 刪除所有符合特定條件的實體 38 /// </summary> 39 /// <param name="predicate">查詢條件謂語表達式</param> 40 /// <returns>操作影響的行數</returns> 41 int Delete(Expression<Func<TEntity, bool>> predicate); 42 43 /// <summary> 44 /// 批量刪除刪除實體 45 /// </summary> 46 /// <param name="entities">實體對象集合</param> 47 /// <returns>操作影響的行數</returns> 48 int Delete(IEnumerable<TEntity> entities);
針對 DTO 的 增、改、刪 業務操作API
在業務層實現對實體的增加,更新操作的時候,如果業務層接收的是 Dto 數據,需要對 Dto 的數據進行合法性檢查,再將 Dto 通過 數據映射組件 AutoMapper 創建或更新相應類型的實體數據模型 Model,然后再按需求對 Model 的導航屬性進行更新,再提交保存。在進行刪除操作的時候,需要使用傳入的主鍵 Id 檢索相應的實體信息,並檢查刪除操作的可行性,再提交到上下文中進行刪除操作,並刪除其他相關數據。在這些針對實體的業務操作中,存在着很多相似的重復代碼,這種重復代碼的存在,會極大降低系統的可維護性。因此,在 數據倉儲操作 中設計了一組專門針對 Dto 的業務操作API,利用 無返回委托 Action<T> 與 有返回委托 Func<T, RT> 來向底層傳遞 各實體業務操作的變化點的業務邏輯,以達到對 Dto 業務重復代碼的徹底重構。
/// <summary> /// 以DTO為載體批量插入實體 /// </summary> /// <typeparam name="TAddDto">添加DTO類型</typeparam> /// <param name="dtos">添加DTO信息集合</param> /// <param name="checkAction">添加信息合法性檢查委托</param> /// <param name="updateFunc">由DTO到實體的轉換委托</param> /// <returns>業務操作結果</returns> OperationResult Insert<TAddDto>(ICollection<TAddDto> dtos, Action<TAddDto> checkAction = null, Func<TAddDto, TEntity, TEntity> updateFunc = null) where TAddDto : IAddDto; /// <summary> /// 以DTO為載體批量更新實體 /// </summary> /// <typeparam name="TEditDto">更新DTO類型</typeparam> /// <param name="dtos">更新DTO信息集合</param> /// <param name="checkAction">更新信息合法性檢查委托</param> /// <param name="updateFunc">由DTO到實體的轉換委托</param> /// <returns>業務操作結果</returns> OperationResult Update<TEditDto>(ICollection<TEditDto> dtos, Action<TEditDto> checkAction = null, Func<TEditDto, TEntity, TEntity> updateFunc = null) where TEditDto : IEditDto<TKey>; /// <summary> /// 以標識集合批量刪除實體 /// </summary> /// <param name="ids">標識集合</param> /// <param name="checkAction">刪除前置檢查委托</param> /// <param name="deleteFunc">刪除委托,用於刪除關聯信息</param> /// <returns>業務操作結果</returns> OperationResult Delete(ICollection<TKey> ids, Action<TEntity> checkAction = null, Func<TEntity, TEntity> deleteFunc = null);
數據查詢 API
一個系統的數據層的所有 API 設計中,看似簡單卻又最復雜的是 數據查詢API 的設計,可能的原因如下:
- 業務需求是未知的,不可預見的,很難設計出能滿足各種業務需求的 數據查詢API
- 業務需求是多變的,在數據層設計 數據查詢API,很難保持穩定,往往也要跟隨業務進行變更
- 業務需求需要的數據是千奇百怪的,數據層 設置死的API很難滿足業務的需求,給多了對系統性能造成影響,給少了又不能滿足業務需求
基於以上理由,可見在數據層中設計 數據查詢API,是很難滿足業務層對數據的需求的。那怎么辦呢?答案就是不把數據查詢的API設置死,把數據查詢的決定權交給“真正需要數據的地方”,在哪需要數據就在哪查詢,需要哪些數據、需要數據的哪一部分,業務層自己說了算,數據層只需要提供一個用於數據查詢的“數據源”即可。到這里,IQueryable<T>的一切特性,幾乎都是為了滿足上面的需求而來的,只需在 IRepository 中對數據層開放一個“只讀的” IQueryable<TEntity> 查詢數據集即可,不同的數據查詢需求,通過設計 IQueryable<T> 的擴展方法來完成。
上面定義的 IQueryable<T>查詢數據源,能滿足大部分數據查詢的需求,但某些 EntityFramework 的特定查詢需求,還是應該單獨定義 數據查詢API,以更好的保障不丟失 EntityFramework 的數據查詢自由度。在這里主要定義了 通過主鍵查找實體、使用 Include 包含指定導航屬性 的數據查詢API:
1 /// <summary> 2 /// 獲取 當前單元操作對象 3 /// </summary> 4 IUnitOfWork UnitOfWork { get; } 5 6 /// <summary> 7 /// 獲取 當前實體類型的查詢數據集 8 /// </summary> 9 IQueryable<TEntity> Entities { get; } 10 11 /// <summary> 12 /// 實體存在性檢查 13 /// </summary> 14 /// <param name="predicate">查詢條件謂語表達式</param> 15 /// <param name="id">編輯的實體標識</param> 16 /// <returns>是否存在</returns> 17 bool ExistsCheck(Expression<Func<TEntity, bool>> predicate, TKey id = default(TKey)); 18 19 /// <summary> 20 /// 查找指定主鍵的實體 21 /// </summary> 22 /// <param name="key">實體主鍵</param> 23 /// <returns>符合主鍵的實體,不存在時返回null</returns> 24 TEntity GetByKey(TKey key); 25 26 /// <summary> 27 /// 獲取貪婪加載導航屬性的查詢數據集 28 /// </summary> 29 /// <param name="path">屬性表達式,表示要貪婪加載的導航屬性</param> 30 /// <returns>查詢數據集</returns> 31 IQueryable<TEntity> GetInclude<TProperty>(Expression<Func<TEntity, TProperty>> path); 32 33 /// <summary> 34 /// 獲取貪婪加載多個導航屬性的查詢數據集 35 /// </summary> 36 /// <param name="paths">要貪婪加載的導航屬性名稱數組</param> 37 /// <returns>查詢數據集</returns> 38 IQueryable<TEntity> GetIncludes(params string[] paths);
至此,OSharp 的數據層定義基本完成,下篇我們將逐條講解 EntityFramework 的數據層實現。
開源說明
github.com
OSharp項目已在github.com上開源,地址為:https://github.com/i66soft/osharp,歡迎閱讀代碼,歡迎 Fork,如果您認同 OSharp 項目的思想,歡迎參與 OSharp 項目的開發。
在Visual Studio 2013中,可直接獲取 OSharp 的最新源代碼,獲取方式如下,地址為:https://github.com/i66soft/osharp.git
nuget
OSharp的相關類庫已經發布到nuget上,歡迎試用,直接在nuget上搜索 “osharp” 關鍵字即可找到
系列導航
本文已同步到系列目錄:OSharp快速開發框架解說系列