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都用的到的功能,也就是所謂的顯示給客戶端的功能,要如何打造一個容易傳遞資訊給顯示端,並且要修改顯示樣式的時候還很簡單。


留待下回分解。

4 則留言 :

  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

      希望有回答到你的問題

      謝謝

      刪除

Related Posts Plugin for WordPress, Blogger...
您或許對這些文章有興趣: