2014年10月2日 星期四

[iThome 第七屆鐵人賽 10] 加上 Unit of Work,抽離Entity Framework的依賴就完美了

在上一篇介紹完了Repository Pattern,我們能夠抽離實際在做儲存的動作,讓我們在替換實際儲存動作更加容易。

但是光靠一個Repository Pattern其實還是有些缺陷,因此,通常來說會實作Unit of Work pattern搭配Repository pattern達到一個比較完美的狀態。

Repository Pattern的問題是什麽

Repository Pattern代表一個儲存,以DB的世界來說,Repository 其實代表的是一個Table。在比較不複雜的程式來說,Repository層級可能就够了,但是如果要到一個複雜一點的程式,Repository pattern就有點相形見拙。

Repository Pattern最大的問題在於,當需要和一個以上的Table溝通的時候,DB的那種Atomic operation就沒有了。因為,Repository是針對Table,所以假設需要同時儲存到兩個Table,可以使用兩個Repository來做。問題在於,這兩個Repository彼此不知道對方,表示,假設Repository 1 儲存完成了,但是Repository 2 儲存失敗了,以完整的Atomic operation來說,只要一個失敗,整個operation應該算是失敗了,但是,因為Repository 之間是沒有聯繫的,因此資料會處於一種dirty state,就是一個進去了,但是一個失敗了。

要解決這個問題,我們就需有一個東西,來管理Repository彼此之間的情況,好讓它可以再一個成功另外一個失敗的情況下,整個roll back處理,而Unit of Work正是一個這樣的Pattern。

什麽是Unit of Work

基本上我們可以把一次的operation想做是一個unit of work。這個operation裡面可能有很多動作,或許需要更新3個table的資料,或許要新增3個table的資料。

這一個operation肯定是所有的動作都完成了,才算是整個operation結束。以DB的角度來想,就是像Transaction一樣的概念。

而Unit of Work這個pattern就是會對這個operation的每一個動作,做一個記錄。直到當被告知完成的時候,它才會真的去做處理,並且只有兩種情況回報:成功,或者失敗。

以DB的世界來說,Unit of Work代表一個DB,而Repository代表一個Table。

Entity Framework的DbContext本身就有做Unit of Work。因此我們才能夠做一些CRUD,然後在一次呼叫SaveChange(),而也是這個時候DbContext才會真的把他有記錄的內容一次對DB做,並且返回成功或失敗。

如果對於Unit of Work有興趣,可以看一下這個Code Project的文章: Unit of Work Design Pattern

Unit of Work的interface定義

/// <summary>
/// 實作Unit Of Work的interface。
/// </summary>
public interface IUnitOfWork : IDisposable
{
/// <summary>
/// 儲存所有異動。
/// </summary>
void Save();

/// <summary>
/// 取得某一個Entity的Repository。
/// 如果沒有取過,會initialise一個
/// 如果有就取得之前initialise的那個。
/// </summary>
/// <typeparam name="T">此Context裡面的Entity Type</typeparam>
/// <returns>Entity的Repository</returns>
IRepository<T> Repository<T>() where T : class;
}

基本上我們這邊需要實作的方法很簡單,一個是如何取得我們的Repository。再來一個就是把所有透過Repository的動作,透過save存入到實體的位置。


Unit of Work的EF 實作


因為EF的DbContext本身就有Unit of Work,因此我們實作起來非常簡單。

/// <summary>
/// 實作Entity Framework Unit Of Work的class
/// </summary>
public class EFUnitOfWork : IUnitOfWork
{
private readonly DbContext _context;

private bool _disposed;
private Hashtable _repositories;

/// <summary>
/// 設定此Unit of work(UOF)的Context。
/// </summary>
/// <param name="context">設定UOF的context</param>
public EFUnitOfWork(DbContext context)
{
_context = context;
}

/// <summary>
/// 儲存所有異動。
/// </summary>
public void Save()
{
_context.SaveChanges();
}

/// <summary>
/// 清除此Class的資源。
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// 清除此Class的資源。
/// </summary>
/// <param name="disposing">是否在清理中?</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_context.Dispose();
}
}

_disposed = true;
}

/// <summary>
/// 取得某一個Entity的Repository。
/// 如果沒有取過,會initialise一個
/// 如果有就取得之前initialise的那個。
/// </summary>
/// <typeparam name="T">此Context裡面的Entity Type</typeparam>
/// <returns>Entity的Repository</returns>
public IRepository<T> Repository<T>() where T : class
{
if (_repositories == null)
{
_repositories = new Hashtable();
}

var type = typeof(T).Name;

if (!_repositories.ContainsKey(type))
{
var repositoryType = typeof(EFGenericRepository<>);

var repositoryInstance =
Activator.CreateInstance(repositoryType
.MakeGenericType(typeof(T)), _context);

_repositories.Add(type, repositoryInstance);
}

return (IRepository<T>)_repositories[type];
}
}

這邊沒有什麼太過於特別的東西,這個實作需要DbContext,而這個DbContext會透過Reflection注入到Repository裡面,因此所有的異動在 DbContext都有記錄,因此,我們做Save的時候,是一個Atomic的transaction。


我這個版本的Unit of work屬於我比較早期就開始使用,因此這邊Repository的取得就沒有使用DI。當然,用不用DI見仁見智。

結語


到了這邊,我們的Unit of Work和Repository就介紹完了。有了這兩個pattern的合作,DAL層的實作就可以完全達到抽象化,避免被綁死在某一個儲存技術。


在下一篇,我們來看一個比較簡單,但是是每一種Application都用的到的功能,也就是所謂的顯示給客戶端的功能,要如何打造一個容易傳遞資訊給顯示端,並且要修改顯示樣式的時候還很簡單。


留待下回分解。


17 則留言 :

  1. 回覆
    1. 如果有什麼不清楚的地方,歡迎問我哦

      刪除
  2. 想跟您請教一下 EFUnitOfWork 裡面的「public void Dispose()」什麼時候才會被觸發!!
    我知道 Controller 有 override Dispose 就能 自動觸發,但在 EFUnitOfWork 什麼時候才會被觸發??

    回覆刪除
    回覆
    1. 對於您的問題,答案是,沒有人呼叫,就永遠不會被呼叫

      Dispose這個方法在.net 裡面屬於 手動呼叫 (Explicit)的資源清理方法 - 換句話說,如果沒有人呼叫的話,.Net Runtime是不會執行的 - 所以,都建議用 using方式來new,會自動清理。

      有另外一種會被.net runtime自動呼叫(Implicit)的資源清理方式,就是所謂的Finalizer(以前稱為Destructor) - 不過這種主要用來設定清理 unmange 的程式代碼

      原則上,如果你是手動呼叫 EFUnitOfWork,那麼呼叫方式都是建議 類似 using(var uow = new EFUnitOfWork()) - 如果有用DI呼叫,看是什麼,想Autofac會自動呼叫Dispose

      SO上面有很多相關問題的討論,可以參考

      http://stackoverflow.com/questions/2871888/dispose-when-is-it-called
      http://stackoverflow.com/questions/732864/finalize-vs-dispose

      希望有回答到你的問題

      謝謝

      刪除
  3. 可以跟您請教個問題
    我參考了 http://www.cnblogs.com/JustRun1983/p/3307774.html 這篇文章和您的文章

    關於 Unit of Work 這個類的撰寫我還不是很清楚 我依照您的文章 寫一個
    IRepository Repository() 方法 但在EFGenericRepository 出錯找不到這類別

    透過nuget發現要抓 NContext.Extensions.Entity.Framework
    本身我自己在學習是使用 EntityFramework version="6.1.3"

    希望前輩指點

    回覆刪除
    回覆
    1. 由於提供的錯誤訊息不夠清楚,我還沒有辦法透過描述來了解問題在哪裡。

      看是否能夠把你目前有嘗試的專案打包(只需要你嘗試的部分即可 - 可以放到github或者丟個鏈接給我),我直接幫你看看可能會快一些
      或者提供一下相關發生位置的錯誤或訊息也可以。

      這個對你幫助應該比較大

      刪除
    2. 我將簡單的 Demo 上傳到 Github
      https://github.com/Yosheng/MVC-Demo 麻煩前輩指點

      前陣子剛接觸 Spring boot 透過 Java 實作Restful服務對於三層架構不算太陌生
      但對於在.NET實現這樣的架構 卻相形見絀 還渴望前輩指點

      刪除
    3. 我有送出一個pull request - 說明這邊也貼一下

      https://github.com/Yosheng/MVC-Demo/pull/1

      1. 你的範例裡面,實作Repository的叫做 Repository 而不是我之前範例的 EFGenericRepository 所以名稱需要調整

      2. 正常來說,Repository的專案會在所有的dependency裡面最下面的那層(換句話說,大家都會依賴他) - 因此調整Reference的順序

      3. uow並沒有實作IUnitOfWork - 因此增加了這層關係

      4. 增加了一個使用uow的範例

      供你參考

      刪除
    4. 謝謝前輩的指點 不勝感激

      刪除
    5. 客氣了 - 如果還有問題歡迎在留言給我
      也歡迎加入我的粉絲頁和持續關注我的部落格。

      刪除
    6. 為什麼會是讓 Entity 參照 Repository 而不是 Repository 參照 Entity?
      三層模式底下 不是先透過 Entity 建立和資料庫之間的關係
      再透過 Repository 進行存取嗎? 麻煩前輩指點..

      因為如果讓 Repository 作為最底層感覺怪怪的 Demo中的 Domain 是Entity而不是 Value Object

      刪除

    7. 補充一下,我上次的描述有點不夠精準

      1. Repository嚴格來說不需要參照entity
      Repository簡單來想可以理解成為和db某個table溝通的一個管道
      Entity代表的是這個db table在物件世界裡面的一個代表
      換句話說 Entity是東西本身,而Repository則是取得這個東西的方式

      由於Repository是透過泛型撰寫的關係 - 只需要呼叫這個Repository的“人”(以3層式架構來說,這個人通常是邏輯層)知道這個Repository要呼叫那個entity就好
      因此是呼叫Repository的那個人需要參照Repository和Entity。但是Repository和entity兩個之間本身是不需要互相有參照。

      因為只需要呼叫的人知道,我今天要什麼(entity),然後是透過什麼方式(ef的Repository)作為橋樑就好。

      所以Repository會是整個程式的最下面一層 - 因為他已經同等於db連線的概念
      (在這個角度來說Entity也會是整個程式的最下面一層,他和Repository應該是平級)

      當然這個的前提是如果使用的方式是用泛型的方式呼叫 - 假設今天不是用泛型的方式呼叫,那麼換句話說每一個entity就會刻一個Repository
      在這樣的情況下,Repository會需要知道entity的type(才有辦法刻),因此就會需要參照

      所以這個完全取決於你最後的做法

      2. 延伸到我上次有個commit調整了你的參照

      正常來說,Repository和Entity會寫在同一個專案 - 因為2個基本上組成了 Data Access Layer這一層
      不過你的範例程式碼Entity在Domain這一層
      然後domain這一層是一個console程式

      從結構上面來說,這個拆解的不夠細,因為會把Data Access Layer (entity)和 邏輯層、UI層(console)混在一起

      造成了,我在console裡面要呼叫範例的時候Domain這個專案要參照Repository(無形中造成Entity 要 參照 Repository的結果)

      我猜測你是在做測試,所以沒有特別提,如果是這種結構,正常來說Entity會是一個獨立的Project,然後Domain會參照Entity和Repository(因為Domain是呼叫的人,還記得第一點提到呼叫的人才需要知道這個關聯關係)
      這樣就會發現Entity和Repository其實不需要互相參照

      以上,供你參考

      刪除
    8. demo
      ├─dal
      │ ├─po
      │ └─vo
      ├─service
      │ ├─bo
      │ ├─impl
      │ └─util
      └─web
      ├─para
      └─util
      上述是我學習 Spring boot 使用的架構
      您所說的 Data Access Layer 相對於我的dal 而po (Persisit Obejct)物件即是存放 entity地方
      此外在dal層都會建立 interface 作為repository 提供給 service 進行呼叫

      對應到asp.net這部分我就不太清楚 很多底層的事情都透過spring boot本身的運作機制做掉
      好比如依賴注入asp.net依照使用不同的framework有不同做法 拿autoface來說需要寫一個類去實作

      而對於資料存取層 ORM的部分 Asp.net藉由Entity Framework來實作 我選擇使用Code First的方式來完成
      所以建立一個 Domain 來存放這些 Entity
      就好比在 Spring boot 藉由Hibernate來實作 建立PO來存放Entity道理一樣

      但是在 Repositroy 的部分 我的設想是透過泛型讓所有的 Entity都有自己的Repo並透過繼承的方式 繼承基本的增刪改查功能
      藉由這種方式來增加查詢的彈性 因為部分查詢比較複雜就直接寫原生 ado.net去實現

      所以您才會看到我的 Repository參照Domain
      也許是我名稱定義不清楚還請前輩指點 我是否將Domain修改為Entity更貼切呢?

      為了實踐低耦合我才將Entity和Repository拆開 方便日後如果不希望選擇Code First只需抽換此層

      程式的進入點 應該是最上層的 Demo (web層 這邊實作MVC架構) 因此我目前應該是還缺乏建立一個 Service
      但對於DAL層的設計還請前輩指點 是否我的想法或作法上有缺失?

      刪除
  4. 首先先聲明一下,由於我這邊很多的知識來源於我從不同管道學習到的東西,因此很有可能有些是不夠準確或者我描述的不夠好
    記得還是保持懷疑的態度,有什麼覺得怪怪我們都能討論看看,畢竟談到架構有時候已經到風格和是不是適合某個情境的情況

    進入正題:

    1. 您列出的結構基本上符合常見的3層式架構,在.net的世界裡面差不多也是如此。
    我是沒有接觸過spring boot所以不太了解他的運作模式,因此沒有辦法給您類似的東西能夠參考。
    不過您提到的依賴注入的確會透過第三方套件來達成(不過其實寫起來還蠻容易) - 題外話現在很夯的asp .net mvc core 就會內建帶一個依賴注入

    2. 命名的部分 - 一樣我覺得這個是風格和只要一致其實取什麼都好 - 我個人是習慣叫 代表DB的table的class叫Entity(應該是因為EF的關係)
    Domain的話比較偏向Service或者系統每個層在溝通之間的Data Transfer Object(DTO)

    3. Repository參照Entity其實沒什麼問題 - 我覺得這個可能是我之前誤導你了 Orz
    同我上篇提到,假設你的Repository會在做一些細部的客製化(例如針對某些特定entity再做一些細部的sql tune)- 那麼Repository參照entity沒什麼問題
    我那個時候應該是手賤,想說剛好有個console在Domain裡面給你看一下執行效果所以才調整了順序。

    4. 關於使用EF這個事情 - 由於你有提到你可能有複雜邏輯會自行下sql query,那麼或許EF不太適合你。
    EF有點重,可以考慮使用一些輕量級的orm(micro orm),例如 Dapper (知名的StackOverflow就是他的創辦人) 就很多人使用。
    基本上輕量級的orm和寫sql差不多,差別在於有一些strong type的輔助。有點像是有ado .net的速度,同時又有orm的概念
    下面提供幾個鏈接供您參考:

    官方repo:https://github.com/StackExchange/Dapper
    黑大寫的介紹:http://blog.darkthread.net/post-2014-05-15-dapper.aspx

    以上有什麼不清楚在和我說

    回覆刪除
    回覆
    1. 前輩提及的 Dapper 我稍微看過 但目前還沒打算深入研究 我想先從EF開始 之後慢慢去嘗試其他的ORM框架
      經過幾天的思考後我參考下述兩篇文章並修改自己的程式碼,還希望前輩指點

      ASP.NET Web API实践系列02,在MVC4下的一个实例, 包含EF Code First,依赖注入, Bootstrap等
      http://www.cnblogs.com/darrenji/p/4049555.html

      這篇文章使用 Unity 作為容器

      Asp.Net MVC+EF+三层架构的完整搭建过程
      http://www.cnblogs.com/zzqvq/p/5816091.html

      這篇文章使用 autoface 作為容器

      此外我也參考 ASP.NET MVC 5 網站開發美學 書上所提及使用 Code first 的方法進行開發
      但這部份我比較困惑是 好像只有設定 Repository 為起始專案才能正常建立資料庫
      倘若設定 Web 為起始專案就無法建立?

      目前我所構思的架構如下
      Models -> 存放交換的物件DTO或者VO
      Repository -> 比對 DAL層 資料增刪改查的方式 不論使用 Code First or Model First or Database First
      Entity -> 對應到資料表的實體類別
      Common -> 繼承會用到的通用類別
      Service -> 比對 BLL層 業務邏輯判斷
      Web -> 實現所謂的MVC架構 目前我打算設計成 API輸出和 View輸出都支援 參考 Web API实践系列02


      碰到的問題是專案之間的參考?不太清楚怎麼設定才能讓 Web 起來的同時 也建立資料庫

      Autofac 不太會使用 我目前建立一個 Container 放在web層 但不知道怎麼注入使用

      最後是我這樣的分層方式 日後我要寫測試類 會不會不容易實做?

      程式碼我依然放在 Github 還請前輩指正
      https://github.com/Yosheng/MVC-Demo

      刪除
    2. code first是在有呼叫到的時候才會觸發建立db - 所以可能你沒用Repository的時候完全沒有呼叫到ef所以才沒觸發

      架構的部分沒有太多意見,基本上我自己開起來也差不多是這樣

      關於 DI注入的部分,其實autofac還蠻容易的(應該說每一個DI注入都蠻容易),只需要特別下載一個mvc integration的套件(Autofac.Mvc5)然後在把你的service註冊進去即可

      這個系列我也有介紹過autofac,可以參考:
      http://blog.alantsai.net/2014/10/BuildYourOwnApplicationFrameworkOnMvc-30-Conclusion.html#WizKMOutline_1414590577032597

      本來在做這個系列要整理一個範例,但是一直沒時間,後來我朋友有用這個系列概念建立一個repo,你也可以參考一下
      https://github.com/matsurigoto/mvc-base-project

      以上,如果還有什麼問題,都歡迎在來回復哦

      刪除
  5. 作者已經移除這則留言。

    回覆刪除