資料操作技巧
分層架構中的 Entity Framework
John Papa
本專欄內容是根據 ADO.NET EntityFramework 的搶鮮版所撰寫的。本文包含的所有資訊均有可能變更。

目錄
當 N 層架構的架構設計師在評估任何新的技術、模式或策略時,他們必須思考如何讓新的東西與架構緊密結合在一起。只要利用 Entity Framework,整合就不是問題。可以整合到 N 層架構,也可以整合到單層架構。
在本月的專欄中,我要示範如何將 Entity Framework 融入到使用 Windows® Communication Foundation (WCF) 和 Windows Presentation Foundation (WPF) 技術及 Model View Presenter (MVP) 模式的 N 層架構中。我會提出一個範例架構,其中包含邏輯存放區資料庫、資料存取、網域模型、商務管理員等層、服務層、展示層及一個被動 UI 層,我也會示範如何使用 Entity Framework 來整合這幾層。我所使用的所有程式碼範例都可以從 MSDN® Magazine 網站下載。
定義分層
我所提出的應用程式可讓使用者搜尋 NorthwindEF 範例資料庫中的客戶,然後檢視、新增、編輯或刪除客戶。在探究程式碼和範例之前,我們先來討論一下範例的整體架構。因為我要強調的重點不是架構本身,而是如何整合 Entity Framework 與架構設計,所以我選擇一個相當常見的架構,這個架構可以很容易隨著其他策略而加以修改和整合。
[圖 1] 顯示一般分層架構的概觀。上兩層使用 UI 層和展示層來處理使用者介面展示和巡覽。UI 層可以用任何一種技術來實作;但在本專欄及其範例中,我選擇使用 WPF。UI 層採用具有被動檢視的 MVP 模式,這表示檢視 (頂端 UI 層) 是由展示層來管理和控制。展示者負責提供資料給檢視、從檢視中取出要儲存在下層的資料,且通常還會負責處理檢視所引發的事件。
圖 1 架構概觀 (按一下影像以放大圖片)
在我的範例中,展示者會經由 WCF 來與下層通訊。展示者會依據服務的合約來透過 WCF 叫用服務。服務層則會透過服務合約介面來公開其服務。這些合約可讓展示者確知如何呼叫服務。
服務層負責接收來自展示者的通訊,然後呼叫適當的商務層方法,以執行適當的商務邏輯並收集或修改資料。本專案的商務邏輯和 LINQ to Entities 程式碼就是位在商務層。LINQ to Entities 程式碼會參考從 Entity Framework 所產生的實體模型。執行 LINQ 查詢時,Entity Framework 會將 LINQ 查詢轉譯為概念實體模型 (實體資料模型,EDM)、將實體觀點對應到儲存層,然後產生可對資料庫執行的 SQL 查詢。
建置模型
我已經說明架構中各層如何運作的概觀,現在讓我們來探討每一層與 Entity Framework 有關的關鍵環節。因為應用程式已經有資料庫,所以我一開始就從 NorthwindEF 資料庫產生實體模型。
首先,我先建置實體模型,接著也將實體對應到資料庫。我們可以用 EDM 精靈來幫忙產生基底實體模型,然後再依需要來修改模型,以納入繼承、實體分割及其他網域模型概念。[圖 2] 顯示 EDM 精靈,其中已選取要匯入到 EDM 的所有資料表和預存程序。
圖 2 從資料庫產生模型 (按一下影像以放大圖片)
使用 EDM 時經常感到困擾的問題之一,就是 EntitySets 和 EntityTypes 的預設命名慣例。在我的網域模型中,我喜歡使用單數名稱來命名所有的實體。我會建立一個 Customer 的執行個體,或使用 List<Order> 來傳回一份 Order 執行個體清單。每一個實體都是一個藍圖的單數執行個體,藍圖有屬性可定義該實體。
另一方面,我喜歡用複數命名慣例來命名 EntitySets。在要求 ObjectContext 參考其 Customers 或 Orders 的集合時,LINQ 查詢中經常會用到 EntitySets。
我們以下面的 LINQ to Entities 查詢為例來說明這一點:
var q = from c in context.Customers
select c;
List<Customer> customerList = q.ToList();
這個查詢會告訴 LINQ to Entities 去存取 Customers EntitySet,然後在執行後傳回所有 Customer 實體執行個體。第二行會執行查詢並傳回 List<Customer> 給區域變數 customerList。在本範例中,EntitySet 是複數,所以一看就知道是查詢 EntitySets,然後傳回 Customer (請注意這是單數) 實體的執行個體。
一定要遵照這種命名慣例嗎?當然不是這樣的。不過,我認為這樣可以讓程式碼看起來更清楚。否則,如果您直接使用 EDM 精靈傳回的預設結果,則會得到名稱為 Customers 的 EntitySets 和名稱為 Customers 的 EntityType,這會讓您的 LINQ to Entities 查詢變成這樣:
var q = from c in context.Customers
select c;
List<Customers> customerList = q.ToList();
當 EDM 精靈產生模型時,可以很容易修改 EntitySet 和 EntityType 名稱。只要在圖表中選取實體,接著在 [屬性] 視窗中檢視其屬性,然後修改想要的設定即可 (請參閱 [圖 3])。就這個應用程式來說,我藉由設定 Name 屬性來將所有 EntityTypes 修改成單數。我沒有變更 EntitySet Name 屬性,因為它本來就是複數。
圖 3 變更 EntityType 名稱 (按一下影像以放大圖片)
運作方式
現在,我要開始示範應用程式,從檢視 (位於 NWUI 專案中) 和展示者 (位於 NWPresentation 專案中) 開始,由頂層往下來討論其運作方式。這兩個專案都可在本專欄所附的程式碼下載中取得。應用程式會載入客戶搜尋檢視,可讓使用者比對公司名稱準則來搜尋客戶 (請參閱 [圖 4])。檢視是使用 WPF 實作,當使用者與檢視互動時,檢視會引發由展示者所接聽的事件,接著,展示者會採取適當的動作。
圖 4 搜尋客戶 (按一下影像以放大圖片)
當使用者搜尋以字母 D 開頭的所有客戶時,如 [圖 4] 所示,檢視會在使用者按一下 [搜尋 (Search)] 按鈕時引發事件。展示者會接聽這個事件,然後藉由透過 WCF 呼叫服務層來回應,以取得一份要顯示在 CustomerSearchView 上的客戶實體清單。以下是使用者在檢視中按一下 [搜尋 (Search)] 按鈕時的程式碼:
private void btnSearch_Click(object sender, RoutedEventArgs e) {
if (FindCustomerSearchResults != null) FindCustomerSearchResults();
}
此程式碼沒有與傳回的實體清單產生互動,而是留給展示者去處理。檢視會使用 WPF 資料繫結來參考實體的屬性,這樣就知道如何將實體清單繫結到清單檢視控制項的項目。檢視與實體之間唯一的互動是透過資料繫結來進行。
CustomerSearchView 會引發事件 FindCustomerSearchResults,而 CustomerSearchPresenter 會接聽事件,然後接手處理並執行搜尋。下列程式碼顯示 CustomerSearchPresenter 類別如何建立 NWServiceClient 類別的執行個體,此類別是下層公開之 WCF 服務的 Proxy:
public void view_FindCustomerSearchResults()
{
if (this.view.CompanyNameCriteria.Length > 0)
using (var svc = new NWServiceClient())
{
IList<Customer> customerList = svc.FindCustomerList( view.CompanyNameCriteria);
view.CustomerSearchResultsList = customerList;
}
}
NWServiceClient 是從 NWPresentation 專案中使用 ServiceReference 來參考,所以展示者知道如何呼叫服務和將要傳回的資料型別。展示層不會也不應該直接參考 EDM。而是,透過 WCF 所公開的 DataContract 來告知可能的實體型別。這可以讓 Entity Framework 的實體透過 WCF 來跨越實體網路界限而傳遞給展示者。
請注意,當這份 Customer 實體清單傳回之後,在檢視上就會設定為 public 屬性。接著,檢視的這個屬性會接受 List<Customer>,並將其繫結至檢視的 DataContext。展示者會提供資料並傳遞資料,而檢視會處理任何特定的檢視繫結 (因為程式碼完全依技術而定,不論是 WPF、Silverlight®、Windows Form 或 ASP.NET)。
此技巧可讓相同的展示者與實作 ICustomerSearchView 介面的任何檢視一起互動。在這個應用程式中,繫結是以 WPF 繫結技巧來處理,使用的是 DataContext。
合約會公開可由服務層呼叫的方法,以及將傳回的實體。在這個應用程式中,我只有一個傳回 Customer 和 Order 實體型別的方法。這表示合約中只會包含這些實體型別。
WCF 會視情況將 WCF DataContract 屬性套用至實體來處理實體的序列化。藉由透過 DataContract 來公開實體,在 UI 層中不必直接參考 EDM 就可以使用實體。
請注意,從 .NET Framework 3.5 SP1 Beta 1 開始,Entity Framework 會支援自動的圖形序列化。例如,如果父實體有相關聯的子實體,則會序列化父實體及其子實體。在範例應用程式中,因為 OrderManager 的 FindOrderList 方法會使用 LINQ to Entities 查詢,這會立即載入每一張「訂單 (Order)」的「訂單明細 (Order Details)」,所以從中間層傳回的每一個 Order 實體都會包含可透過其巡覽屬性存取的 List<OrderDetail>。
雖然序列化實體可透過 WCF 在展示者與服務層之間傳遞,但 ObjectContext 不會序列化,也不會傳遞給展示者。這表示實體可以在 UI 層使用,但 ObjectContext 會留在下層,這樣才能存取 EDM 和 Entity Framework 的完整資源。
將 ObjectContext 留下表示不能直接在 UI 層中用來擷取或修改實體,也不能在 UI 層中用來管理變更追蹤。無論如何,這些角色最好都留給下層。但是,當實體往下傳回到下層時,應用程式就必須與 ObjectContext 同步處理,這樣才能保存實體中的任何變更。
當使用者按一下 [圖 4] 所示的 [搜尋 (Search)] 按鈕時,展示者會呼叫服務層,再由服務層轉而呼叫商務層 (在 NWBusinessManagers 專案中) 來擷取 List<Customer>。這一層有兩個重要角色。第一個角色是在 EDM 中取得或放置任何資料。第二個角色則是處理任何可能存在的商務邏輯。
CustomerManager 會使用 ObjectContext 來處理與 EDM 的互動,所以就定義一個區域欄位,稱為內容 (context),並於建構函式中建立其執行個體。每一個方法中都可以建立和終結 ObjectContext。不過,最好視需要來開啟和關閉資料庫連接資源。另外,由於可透過類別來存取 ObjectContext,所以不必在類別內傳遞一連串的 private 方法,也就能夠維護變更追蹤。
public CustomerManager()
{
context = new NWEntities();
}
請注意,就這種應用程式而言,ObjectContext 不應該一直保留不放,而是應該視需要來建立/終結。由於識別解析的緣故,緊抓同一個物件內容不放,最後會導致資料不一致和過時,且在執行識別解析時會造成效能退化 (因為要追蹤的資料愈來愈多),在多執行緒環境中,甚至會導致更新發生問題。
下列程式碼顯示商務層中之 CustomerManager 類別的 FindCustomerList 方法。這個方法會宣告 LINQ to Entities 查詢,此查詢會存取內容來要求以準則開頭的一份 Customer 實體清單。執行這個查詢時,它會評估概念層至儲存層的對應,然後產生適當的 SELECT 命令:
public List<Customer> FindCustomerList(string companyName)
{
var q = from c in context.Customers
where c.CompanyName.StartsWith(companyName)
select c;
return q.ToList();
}
如果喜歡的話,您也可以使用 SQL Server® Profiler 來檢視正在執行的查詢。
保存變更
現在,我已經使用簡單的擷取來逐步解說應用程式,接下來探討如何保存對於資料所做的修改。當使用者編輯客戶時,CustomerView 檢視會顯示繫結至適當的 Customer 實體執行個體 (請參閱 [圖 5])。CustomerView 會向展示者引發事件,然後由展示者轉而從下層要求 Customer 實體執行個體。
圖 5 編輯客戶 (按一下影像以放大圖片)
當使用者修改客戶並加以儲存時,就會利用 [圖 6] 所示的程式碼,將實體從展示者傳遞到下層。此程式碼會評估使用者是新增或修改客戶,然後會呼叫適當的服務層方法,同時傳遞實體。

圖 6 展示者中的 SaveCustomer
public virtual void view_SaveCustomer()
{
Customer customer = view.CurrentCustomer;
var svc = new NWServiceClient();
switch (view.Mode)
{
case ViewMode.EditMode:
svc.UpdateCustomer(customer);
break;
case ViewMode.AddMode:
svc.AddCustomer(customer);
break;
default:
break;
}
view.CurrentCustomer = FindCustomer();
}
接著,服務層就將控制權移交給商務層,再由商務層將客戶實體儲存到資料庫。因為客戶實體已經不再是 ObjectContext 的一部分,所以必須先利用 ObjectContext 的 Attach 方法,重新與一個 ObjectContext 結合,如下列程式碼所示。當實體附加至內容之後,實體的屬性必須標示為已修改。做法是利用內容的 ObjectStateManager,並對每一個屬性叫用 SetModified 方法。現在,內容知道實體已被修改,所以可以發出 SaveChanges 方法,這個方法接著會產生 SQL UPDATE 命令,然後對資料庫執行這個命令:
public void UpdateCustomer(Customer customer)
{
context.Attach(customer);
customer.SetAllModified(context); // custom extension method
context.SaveChanges();
}
請注意,UpdateCustomer 方法中的程式碼會使用我命名為 SetAllModified<T> 的擴充方法,對於要修改的實體,這個方法可以輕鬆地設定所有屬性的狀態。SetAllModified<T> 會根據給定的實體 T 來取得 ObjectStateEntry 的執行個體。然後,它會擷取該實體的所有屬性名稱清單,並對每一個屬性反覆地呼叫 SetModifiedProperty:
public static void SetAllModified<T>(this T entity, ObjectContext context)
where T : IEntityWithKey
{
var stateEntry = context.ObjectStateManager. GetObjectStateEntry(entity.EntityKey);
var propertyNameList = stateEntry.CurrentValues.DataRecordInfo. FieldMetadata.Select
(pn => pn.FieldType.Name);
foreach (var propName in propertyNameList)
stateEntry.SetModifiedProperty(propName);
}
另一種可確實儲存實體的方法是呼叫內容的 Refresh 方法。這會告知內容去取得實體執行個體的資料,並將屬性值更新為資料庫的值。ClientWins 的 RefreshMode 列舉值會將原始值取代為資料庫中最新的值,亦即採用「後進先寫入」的策略。
StoreWins 的 RefreshMode 會將實體快取中的原始值和目前值同時覆寫成資料庫的值。ClientWins 是適合後進先寫入的策略,但如果您想要取消變更,並將 UI 的檢視更新成最新的資料庫值,則 StoreWins 是較適合的策略:
context.Refresh(RefreshMode.ClientWins, customer); // Last in wins
Entity Framework 在產生 update 和 delete 命令時會強制執行最佳並行處理。做法是對於 ConcurrencyMode 屬性 (Attribute) 值設定為 Fixed 的任何屬性 (Property),一律將原始值放入其 WHERE 子句中。
根據預設,產生的模型不會將任何欄位指定為並行欄位。這表示當使用者儲存變更時,可能不小心就會覆寫另一位使用者所做的變更。如果有另一位使用者在 CustomerView 開啟時變更某個值,而您想要使用最佳並行處理,您可以在概念模型中設定 EntityType 的 ConcurrencyMode 屬性。
編輯 EDM 檔案並將 ConcurrencyMode 設定為 Fixed,就是告訴 Entity Framework 將這個資料行加入至任何 Update 或 Delete 命令的 WHERE 子句。因此,如果找不到相符的資料列,就會引發 OptimisticConcurrencyException。[圖 7] 顯示當我在資料庫中修改客戶的區域時,就在使用者嘗試修改同一個區域之前,就會引發這個例外狀況。
圖 7 OptimisticConcurrencyException (按一下影像以放大圖片)
您可以攔截這個例外狀況,並採取任何適當的動作。例如,您以攔截例外狀況、記錄例外狀況,然後強制覆寫使用者的變更,如下所示:
catch (OptimisticConcurrencyException e){
context.Refresh(RefreshMode.ClientWins, customer); // Last in wins
logger.Write(e);
context.SaveChanges();
}
刪除和新增
當使用者刪除客戶時,CustomerManager 的 DeleteCustomer 方法會取得客戶實體並執行刪除作業:
context.Attach(customer);
context.DeleteObject(customer);
context.SaveChanges();
首先,必須使用 Attach 方法,將 Customer 實體執行個體與 ObjectContext 重新結合。接著,必須從 ObjectContext 中刪除客戶。如此一來,ObjectContext 內的變更追蹤機制就會知道 Customer 實體執行個體已遭刪除。最後,當呼叫 SaveChanges 方法時,ObjectContext 會知道已刪除實體,且應該會產生並執行 DELETE SQL 命令。
當新增客戶時,CustomerManager 的 AddCustomer 方法會取得客戶實體,並執行插入作業,如下所示:
context.AddToCustomers(customer);
context.SaveChanges();
這個實體執行個體是新的,所以必須加入至內容並以旗標標示成 Customer 實體的新執行個體,做法是利用 AddToCustomer 方法,讓 Customer 實體執行個體與 ObjectContext 產生關聯。最後,當呼叫 SaveChanges 方法時,ObjectContext 會知道已新增實體,且應該會產生並執行 INSERT SQL 命令。
結論
我已經示範 Entity Framework 如何整合到架構中、如何使用現代的模式 (例如 MVP 模式),也論及常見的架構問題。Entity Framework 在分層架構中的重點包括:變更追蹤機制、與 LINQ to Entities 的整合、與 ObjectContext 中斷連接和重新連接的能力,並提供開發人員處理並行問題的方法。
John Papa (
johnpapa.net) 是 ASPSOFT (
aspsoft.com) 的資深顧問,也是一個棒球迷,在球賽期間都會在家人的陪伴下一起為洋基隊加油。John 是 C# MVP 和 INETA 演講者,發表過許多著作,目前正在寫他最新的一本書,書名是
Data Access with Silverlight 2。他時常在 DevConnections 和 VSLive 等會議上發表演說。