【翻譯】在Entity Framework 4.0中使用 Repository 和 Unit of Work 模式

【原文地址】Using Repository and Unit of Work patterns with Entity Framework 4.0
【原文發表日期】 16 June 09 04:08 PM

如果你一直在關注這個博客的話,你知道我最近在討論我們加到Entity Framework 4.0中的POCO功能的方方面面,新加的POCO支持促成了在Entity Framework中實現透明性持久的新方式,而該方式在Entity Framework 3.5中是無法實現的。

如果你錯過了我的POCO系列,爲方便起見,我在下面列了出來,快速瀏覽一下也許是個不錯的主意。

POCO in Entity Framework : Part 1 – The Experience(【翻譯】實體框架中的POCO支持 - 第一部分 - 體驗

POCO in Entity Framework : Part 2 – Complex Types, Deferred Loading and Explicit Loading (【翻譯】實體框架中的POCO支持 - 第二部分 - 複雜類型,延遲裝載和顯式裝載

POCO in Entity Framework : Part 3 – Change Tracking with POCO (【翻譯】實體框架中的POCO支持 - 第三部分 - POCO的變動跟蹤

在這個貼子裏,我想要看一下如何將我們的例子做進一步擴充,使用一些諸如RepositoryUnit Of Work這樣常見的模式,這樣我們可以在我們的例子中實現一些特定於持久性的關注。

將我們基於Northwind的例子做一下擴充,譬如說,我有興趣支持下列與Customer(客戶)實體相關的操作:

  • 通過ID來查詢客戶
  • 通過名字來查詢客戶
  • 往數據庫中加一個新客戶

我還想能夠基於ID來查詢產品。

最後,給定一個客戶,我想要能夠往數據庫中加一個Order(訂單)。

在進入細節之前,我想先說明兩件事情:

  • 處理這個問題,“正確”的方式不止一個。在過程中,我會做許多簡化的假設,其目的是展示一幅非常高層次的草圖,示範如何實現這2個模式來解決手上的問題。
  • 通常來說,在使用TDD時,我會從測試開始,使用我的測試來逐步展開我的設計。因爲在這個例子中我沒有按TDD做,請耐心點,如果你看到我在做象預先定義接口這樣的事情,而不是讓測試和共用的東西來主宰接口的必要性,等等。

實現 Repository 模式

讓我們先開始實現針對Customer實體的操作,看一下Customer的repository的樣子:

public interface ICustomerRepository
{
Customer GetCustomerById(string id);
IEnumerable<Customer> FindByName(string name);
void AddCustomer(Customer customer);
}

這個repository接口看上去滿足有關Customer的所有需求:

  • GetCustomerById 應該允許我通過主鍵來得單個客戶實體
  • FindByName 應該允許我查詢客戶
  • AddCustomer 應該允許我往數據庫中加一個客戶

這暫時聽上去不錯,象這樣,爲你的repository定義一個接口是個好主意,特別是你對使用mocks(模擬對象) 或 fakes (假冒對象)來編寫測試感興趣的話,接口允許你完全不用考慮數據庫,促成更好的單元測試。在將來會有更多的博客貼子討論可測試性,mocks 和 fakes,等等。

你也許可以進一步擴展這個接口定義,定義一個共用的IRepository,來處理多個repository類型中共有的關注。如果對你有用的話,這是件值得做的事。在這個特定的例子中,我沒看到其必要性,所以我就免了。但在你添加更多的repository以及重構時,這完全有可能會變得非常重要。

讓我們拿這個repository來看一下如何利用Entity Framework來做一個實現,允許數據訪問:

首先,我需要一個可以用來查詢數據的ObjectContext。你也許想作爲repository構造器的一部分生成一個ObjectContext,但最好還是把那個關注從repository中去除,在別的地方處理爲好。

這是我的構造器:

public CustomerRepository(NorthwindContext context)
{
if (context == null)
throw new ArgumentNullException("context");

_context = context;
}

在上面的代碼片段中,NorthwindContext是個我自己的ObjectContext類型。

讓我們來提供 ICustomerRepository 接口所需的方法的實現。

GetCustomerById 非常容易實現,多虧了LINQ。使用標準的LINQ運算符,我們可以象這樣實現 GetCustomerById

public Customer GetCustomerById(string id)
{
return _context.Customers.Where(c => c.CustomerID == id).Single();
}

類似地,FindByName 可以象這樣。 再一次,LINQ支持使得其實現非常容易:

public IEnumerable<Customer> FindByName(string name)
{
return _context.Customers.Where( c => c.ContactName.StartsWith(name)
).ToList();
}

注意,我選擇將結果呈示爲IEnumerable<T>,你也許會選擇將這呈示爲IQueryable<T>。這麼做有其含意,在這個情形下,我對呈示建立在repository返回結果之上的基於IQueryable的另外的查詢組合,不是很感興趣。

最後,讓我們來看一下可以如何實現AddCustomer

public void AddCustomer(Customer customer)
{
_context.Customers.AddObject(customer);
}

你也許想把保存功能作爲AddCustomer方法的一部分來實現。那麼做也許對這個簡單的例子沒問題,但一般來說,這不是個好主意,這正是Unit of Work出場的地方,過一會兒,我們會看到如何使用這個模式來允許我們實現和協調Save行爲。

這裏是使用Entity Framework來處理持久性的CustomerRepository的完整實現:

public class CustomerRepository : ICustomerRepository
{
private NorthwindContext _context;

public CustomerRepository(NorthwindContext context)
{
if (context == null)
throw new ArgumentNullException("context");

_context = context;
}

public Customer GetCustomerById(string id)
{
return _context.Customers.Where(c => c.CustomerID == id).Single();
}

public IEnumerable<Customer> FindByName(string name)
{
return _context.Customers.Where(c => c.ContactName.StartsWith(name))
.AsEnumerable<Customer>();
}

public void AddCustomer(Customercustomer)
{
_context.Customers.AddObject(customer);
}
}

這裏是我們可以如何在客戶端代碼中使用該repository:

CustomerRepository repository = new CustomerRepository(context);
Customer c = new Customer( ... );
repository.AddCustomer(c);
context.SaveChanges();

在處理與ProductOrder相關的需求時,我可以定義下列接口(建造類似CustomerRepository一樣的實現)。爲簡便起見,我把其細節省略了。

public interface IProductRepository
{
Product GetProductById(int id);
}

public interface IOrderRepository
{
void AddOrder(Order order);
}

使用ObjectContext實現 Unit of Work (工作單元) 模式

你也許已經注意到了,儘管我們沒有實現任何特定的模式來明確地地允許我們將相關操作聚合進一個工作單元(unit of work),但我們已經通過NorthwindContext(我們的ObjectContext類)免費獲取了Unit of Work功能。

其想法是,我可以使用Unit of Work將一套相關的操作聚合在一起,Unit of Work記錄我感興趣的變動,直到我準備將它們保存到數據庫爲止。最終,當我準備保存時,我可以那麼做。

我可以象這樣定義一個“Unit of Work”接口:

public interface IUnitOfWork
{
void Save();
}

注意,在Unit of Work中,你也許還會選擇實現Undo / Rollback功能。在使用Entity Framework時,推薦的undo做法是,丟棄你的上下文以及你想undo的變動。

我已經提到,我們的ObjectContext類型(NorthwindContext)在極大程度上支持Unit of Work模式。 基於我剛定義的契約,爲了使得事情更爲明確一點,我可以將NorthwindContext類改成實現IUnitOfWork接口:

public class NorthwindContext : ObjectContext, IUnitOfWork
{
public void Save()
{
SaveChanges();
}

. . .

在做了這個變動之後,我需要對我們的repository實現做個小的調整:

public class CustomerRepository : ICustomerRepository
{
private NorthwindContext _context;

public CustomerRepository(IUnitOfWork unitOfWork)
{
if (unitOfWork == null)
throw new ArgumentNullException("unitOfWork");

_context = unitOfWork as NorthwindContext;
}

public Customer GetCustomerById(string id)
{
return _context.Customers.Where(c => c.CustomerID == id).Single();
}

public IEnumerable<Customer> FindByName(string name)
{
return _context.Customers.Where(c => c.ContactName.StartsWith(name))
.AsEnumerable<Customer>();
}

public void AddCustomer(Customer customer)
{
_context.Customers.AddObject(customer);
}
}

 

就這麼簡單!現在,我們有了一個對IUnitOfWork友好的repository,你甚至可以使用基於IUnitOfWork的上下文來協調跨越多個repository的工作。下面是一個將訂單加到數據庫的例子,需要多個repository的工作來查詢數據,最終將記錄保存回數據庫中去:

IUnitOfWork unitOfWork = new NorthwindContext();

CustomerRepository customerRepository = new CustomerRepository(unitOfWork);
Customer customer = customerRepository.GetCustomerById("ALFKI");

ProductRepository productRepository = new ProductRepository(unitOfWork);
Product product = productRepository.GetById(1);

OrderRepository orderRepository = new OrderRepository(unitOfWork);

Order order = new Order(customer);
order.AddNewOrderDetail(product, 1);

orderRepository.AddOrder(order);

unitOfWork.Save();

非常有趣地看到,爲了在Entity Framework之上使用Repository 和 Unit of Work模式來訪問數據,我們要編寫的代碼是如此地少。Entity Framework的LINQ支持以及原裝的Unit of Work功能使得在Entity Framework之上建立repository簡單之極。

另一個要注意的事情是,我在這個貼子裏討論的很多東西,都不是與Entity Framework 4.0特別有關,所有這些一般原理在Entity Framework 3.5下也適用。但要能夠象上面展示的那樣,在POCO支持的基礎之上,使用RepositoryUnit of Work,確實顯示了Entity Framework 4.0中的威力。我希望你會發現這非常有用。

最後,我要重申一下,有很多方式可以處理這個題目,在應用Entity Framework, Repository 和 Unit of Work時,還有許多變種方案可以符合你的需求。你也許會選擇實現公共的IRepository接口,你也許還會選擇實現Repository<TEntity>基類,來給予你一些跨越所有repository的常用repository功能。

嘗試一些方法,看哪個最適用於你。我在這裏討論的只是一般的指南而已,但我希望這足夠讓你起步。更重要的是,我希望這示範瞭如何可以使用我們在Entity Framework 4.0中引進的POCO支持,來幫助你建造Entity Framework友好的領域模型,而不必違背透明持久性方面的基本原則。

包含我上面一些示範代碼的項目附在本貼之後。在我們的將來貼子中討論單元測試,TDD和可測試性時,我們還將對這個題目做更多的討論,敬請期待。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章