程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 數據點:分層式體系結構中的實體框架

數據點:分層式體系結構中的實體框架

編輯:關於.NET

本專欄基於 ADO.NET EntityFramework 的預發布版本撰寫而成。文中包含的所有信息均有可能發生變更。

目錄

定義層

構建模型

工作原理

保持更改

刪除和添加

總結

當 n 層體系結構的架構師評估任何新技術、模式或策略時,他們必須考慮新的謎團將如何與體系結構相融合。有了實體框架,集成將不再是問題。它可以集成到 n 層體系結構以及單層體系結構中。

在本月專欄中,我將介紹如何使實體框架能夠適合於使用 Windows® Communication Foundation (WCF)、Windows Presentation Foundation (WPF) 技術以及 Model View Presenter (MVP) 模式的 n 層體系結構。我將演示一個示例體系結構,其中包含邏輯存儲數據庫層、數據訪問層、域模型層、業務經理層、服務層、表示層以及被動 UI 層,同時我還將介紹如何使用實體框架來集成這些層。我使用的所有代碼示例都可以從《MSDN® 雜志》網站下載。

定義層

我將要展示的應用程序允許用戶在 NorthwindEF 示例數據庫中搜索客戶並對其執行查看、添加、編輯或刪除操作。在深入探究代碼和示例之前,讓我們先討論一下此示例的整體體系結構。由於我的重點並不在體系結構本身,而是如何將實體框架與體系結構設計相集成,因此我選擇了一個比較常見的體系結構,它可以在經過修改後非常方便地與其他策略相集成。

圖 1 顯示了典型的分層式體系結構的高級視圖。頂部的兩層使用 UI 層和表示層來處理用戶界面表示和導航。UI 層可通過各種技術來實現;但是,在本專欄的相關示例中,我將使用 WPF。UI 層遵從帶有被動視圖的 MVP 模式,這意味著視圖(頂部 UI 層)由表示層進行管理和控制。表示器負責為這些視圖提供數據、從視圖中抽取數據以保存在較低層,一般情況下還負責響應由視圖引發的事件。

圖 1 體系結構概述

在此處的示例中,表示器通過 WCF 與較低的層進行通信。表示器使用服務的約定作為指導通過 WCF 來調用該服務。服務層通過服務約定接口提供服務。利用這些約定,表示器可以確定如何調用服務。

服務層負責接收來自表示器的通信並調用相應的業務層方法,這些方法將執行相應的業務邏輯和數據收集或修改操作。業務層是業務邏輯和此項目的 LINQ to Entities 代碼將要駐留的位置。LINQ to Entities 代碼將引用從實體框架所生成的實體模型。執行 LINQ 查詢時,實體框架會將 LINQ 查詢轉換為概念實體模型(實體數據模型或 EDM)、將實體內容映射到存儲層、生成 SQL 查詢並針對數據庫加以執行。

構建模型

現在我已提供了對各個層在體系結構中的工作方式的深層次說明,接下來讓我們看一下當各個層與實體框架關聯時要注意的關鍵問題。由於應用程序的數據庫已經存在,因此我使用 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 Name

工作原理

現在我將演示此應用程序並通過視圖(位於 NWUI 項目中)和表示器(位於 NWPresentation 項目中)來討論它如何從頂層向下開始運行。這兩個項目均可從本專欄隨附的代碼下載中獲得。此應用程序會加載客戶搜索視圖,允許用戶通過匹配公司名稱這一條件來搜索客戶(請參見圖 4)。此視圖是使用 WPF 實現的,當用戶與其交互時,它會引發一些事件,其表示器能夠偵聽到這些事件並隨後采取相應的操作。

圖 4 搜索客戶

如果用戶搜索以字母 D 開頭的所有客戶(如圖 4 所示),則當用戶單擊“搜索”按鈕時,此視圖會引發一個事件。表示器會偵聽此事件,並通過在 WCF 中調用服務層來作為響應,從而獲取將要顯示在 CustomerSearchView 中的客戶實體的列表。以下是用戶單擊“搜索”按鈕時視圖中的代碼:

private void btnSearch_Click(object sender,   RoutedEventArgs e) {
   if (FindCustomerSearchResults != null)     FindCustomerSearchResults();
 }

此代碼並不與返回的實體列表交互,而是將其留給表示器來處理。視圖使用 WPF 數據綁定來引用實體的屬性,因此它知道如何將實體列表綁定到列表視圖控件的元素。視圖與實體間存在的唯一交互是通過數據綁定完成的。

CustomerSearchView 會引發事件 FindCustomerSearchResults,而 CustomerSearchPresenter 會偵聽此事件,然後接管任務並繼續執行搜索作為對此的響應。以下代碼顯示了 CustomerSearchPresenter 類如何創建 NWServiceClient 類的實例,其中 NWServiceClient 類是在較低層提供的 WCF 服務的代理:

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 是通過 ServiceReference 從 NWPresentation 項目引用的,因此表示器知道應如何調用服務以及將會返回哪些類型的數據。表示層不會也不應當直接引用 EDM。相反,應該通過 WCF 所提供的 DataContracts 來告訴它應使用哪些類型的實體。這樣即可通過 WCF 將實體框架中的實體跨物理網絡邊界傳遞給表示器。

請注意,此 Customer 實體列表一經返回,即會被設置為視圖的公共屬性。此視圖屬性隨後將接受 List<Customer> 並將其綁定到視圖的 DataContext。表示器會提供數據並進行傳遞,然後由視圖處理任何特定於視圖的綁定(因為該代碼技術針對性特別強,可能會涉及 WPF、Silverlight®、Windows Forms 或 ASP.NET 等領域)。

此方法允許使用同一個表示器與實現 ICustomerSearchView 接口的所有視圖進行交互。此應用程序是使用 DataContext 並通過 WPF 綁定技術對綁定進行處理的。

約定會公開可在服務層中調用的方法以及將被返回的實體。在此應用程序中,我只有一個返回 Customer 和 Order 實體類型的方法。這意味著只有這些實體類型才會包括在約定中。

WCF 通過根據需要將 WCF DataContract 屬性應用到實體中來處理實體的序列化。通過 DataContracts 提供實體,可在 UI 層中使用這些實體而無需直接引用 EDM。

請注意,從 .NET Framework 3.5 SP1 Beta 1 開始,實體框架就已支持自動圖形序列化。例如,如果某個父實體具有關聯的子實體,則該父實體及其子實體都將被序列化。在示例應用程序中,由於 OrderManager 的 FindOrderList 方法使用了為每個 Order 預先加載 Order Details 的 LINQ to Entities 查詢,因此從中間層返回的每個 Order 實體都將包含可通過其導航屬性訪問的 List<OrderDetail>。

雖然序列化實體可通過 WCF 在表示器和服務層之間傳遞,但是 ObjectContext 既不會被序列化也不會傳遞給表示器。這意味著這些實體可在 UI 層中使用,但 ObjectContext 會留在較低層,在那裡它可以訪問 EDM 和實體框架的全部資源。

捨棄 ObjectContext 意味著無法使用它在 UI 層中直接檢索或修改實體,也無法使用它在 UI 層中管理更改跟蹤。總之,這些角色原則上被留在了較低層。但是當實體被傳回較低層時,應用程序必須與 ObjectContext 同步,以便可以保留在實體中所做的任何更改。

用戶單擊圖 4 中的“搜索”按鈕後,表示器會調用服務層,而服務層隨後會調用業務層(位於 NWBusinessManagers 項目中)來檢索 List<Customer>。此層具有兩個主要角色。第一個角色將從 EDM 獲取數據或將數據放入其中。第二個角色將處理可能存在的任何業務邏輯。

CustomerManager 使用 ObjectContext 處理與 EDM 的交互,因此它將定義一個名為 context 的本地字段並在其構造函數中創建一個自身的實例。ObjectContext 可以在每個方法中創建和銷毀。但是,它已被優化為根據需要打開和關閉數據庫連接資源。另外,通過使 ObjectContext 在整個類中都可訪問,它將能夠一直跟蹤更改,而不必在類中使用一系列專用方法進行傳遞:

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 方法重新結合一個,如以下代碼所示。實體被附加到上下文後,必須將實體的屬性標記為已修改。使用上下文的 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

生成更新和刪除命令時,實體框架會強制執行最優的並發操作。為此,需要將原始值包括在任意屬性的 WHERE 子句中並將 ConcurrencyMode 屬性值設置為 Fixed。

默認情況下,在生成模型時字段不會被指定為並發字段。這意味著當某個用戶保存其所做的更改時,他可能會意外覆蓋其他用戶的更改結果。如果其他用戶在 CustomerView 打開的情況下更改了某個值,則當您想要使用最優並發操作時,可以通過在概念模型中設置 EntityType 的 ConcurrencyMode 屬性來達到此目的。

編輯 EDM 文件並將 ConcurrencyMode 設置為 Fixed 時,將告訴實體框架將此列添加到任意 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();

首先,Customer 實體實例必須使用 Attach 方法重新結合一個 ObjectContext。然後必須從 ObjectContext 中將該客戶刪除。這樣,ObjectContext 中的更改跟蹤機制會了解到 Customer 實體實例已被刪除。最後,當調用 SaveChanges 方法時,ObjectContext 會了解到實體已被刪除,自己應生成 DELETE SQL 命令並執行它。

刪除客戶時,CustomerManager 的 AddCustomer 方法會獲取該客戶實體並應用插入操作,如下所示:

context.AddToCustomers(customer);
context.SaveChanges();

由於這是一個新實例,因此必須使用 AddToCustomer 方法將 Customer 實體實例與 ObjectContext 相關聯,以將其添加到上下文中並標記為 Customer 實體的新實例。最後,當調用 SaveChanges 方法時,ObjectContext 會了解到實體已被添加,自己應生成 INSERT SQL 命令並執行它。

總結

在本文中,我演示了如何將實體框架集成到體系結構中、如何使用一些最新的模式(如 MVP 模式)以及如何處理常見的體系結構問題。分層式體系結構中實體框架的主要內容包括它的更改跟蹤機制、與 LINQ to Entities 的集成、斷開和重新連接其 ObjectContext 的能力以及為開發人員提供的處理並發問題的方法。

請將您想向 John 詢問的問題和提出的意見發送至 [email protected]

John Papa (johnpapa.net) 是 ASPSOFT (aspsoft.com) 的一位資深顧問,同時也是一位狂熱的棒球謎,在夏季的大多數夜晚,他都會與家人一起為美國的洋基隊吶喊助威。John 是 C# 領域的一位 MVP 和 INETA 發言人,他曾撰寫過多本著作,現在正忙於撰寫新書 《Data Access with Silverlight 2》。他經常在一些會議(如 DevConnections 和 VSLive)上發表演講。

代碼下載位置: DataPoints2008_07.exe (3,549 KB)

http://download.microsoft.com/download/f/2/7/f279e71e-efb0-4155-873d-5554a0608523/DataPoints2008_07.exe

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved