1. 程式人生 > >如何搭建.NET Entity Framework分散式應用系統框架

如何搭建.NET Entity Framework分散式應用系統框架

一、前言

ADO.NET EntityFramework(以下簡稱EF)是微軟推出的一套O/RM框架,如果用過Linq To SQL的人會比較容易理解,因為Linq To SQL是微軟在.net FrameWork 3.0時推出的一套輕量級的O/RM框架,但是隻支援SQL Server一種資料庫。至.net FrameWork3.5 sp1時,才推出Entity FrameWork,可以通過實現不同的Provider來支援不同的資料庫(當然微軟還是隻內建SQL ServerProvider,其它資料庫的Provider麼,需要第三方開發)。EF加上linq,這是.net開發上的一個巨大進步,

.net程式設計師以物件方式操作資料,以類sql語法在程式裡查詢資料,大大減少了繁瑣的構造SQL語句的工作,可以更加專注於編寫業務邏輯程式碼。但是在多層架構的分散式應用系統中,實體物件通過遠端序列化到客戶端時,這些實體會與其資料上下文(也就是實體容器)分離,在客戶端無法對實體直接進行查詢以及CUDCreate,Update,Delete操作,下面以SQL Server為資料庫,Remoting+Entity Framework3.5作為資料服務層,WinForm作為客戶端,講述一下如何使用EF框架搭建多層分散式應用系統。

二、預備知識

1.Linq To SQL或者 EF方面的基礎知識。此處不作贅述。

2.分散式應用系統方面的基礎知識。所謂分散式應用系統,對於.net而言,就是使用了諸如COM+,.netRemoting,XML WEB Service,WCFMSMQ等遠端呼叫技術的多層架構應用系統,所以需要對上述技術有一定了解。此處不作贅述。

3.面向物件程式設計的一些基礎知識。此處不作贅述。

三、技術分析

1.通過遠端客戶端傳輸過來的實體,都是處於分離狀態(EntityState屬性值為Detached),所以在多層應用程式中的服務端實現實體的更新或刪除時,關鍵是如何把實體附加回實體容器中。MSDN上關於對分離實體的查詢和CUD操作描述如下:

1)附加物件(實體框架)

在實體框架的某個物件上下文內執行查詢時,返回的物件會自動附加到該物件上下文。還可以將從源而不是從查詢獲得的物件附加到物件上下文。您可以附加以前分離的物件、由

 NoTracking 查詢返回的物件或從物件上下文的外部獲取的物件。還可以附加儲存在 ASP.NET 應用程式的檢視狀態中的物件或從遠端方法呼叫或 Web 服務返回的物件。

使用下列方法之一將物件附加到物件上下文:

·呼叫 ObjectContext 上的 AddObject 將物件附加到物件上下文。當物件為資料來源中尚不存在的新物件時採用此方法。

·呼叫 ObjectContext 上的 Attach 將物件附加到物件上下文。當物件已存在於資料來源中但當前尚未附加到上下文時採用此方法。有關更多資訊,請參見如何:附加相關物件(實體框架)。

·呼叫 ObjectContext  AttachTo,以將物件附加到物件上下文中的特定實體集。如果物件具有 null(在 Visual Basic 中為 NothingEntityKey 值,也可以執行此操作。

·呼叫 ObjectContext 上的 ApplyPropertyChanges。當物件已存在於資料來源中,並且分離的物件具有您希望儲存的屬性更新時採用此方法。如果簡單地附加該物件,則屬性更改將丟失。有關更多資訊,請參見如何:應用對已分離物件的更改(實體框架)。

2)應用對已分離物件的更改(實體框架)示例程式碼

privatestaticvoid ApplyItemUpdates(SalesOrderDetail updatedItem)

         {

// Define an ObjectStateEntry andEntityKey for the current object.

EntityKey key;

object originalItem;

using (AdventureWorksEntities advWorksContext =

newAdventureWorksEntities())

            {

try

                {

// Create the detached object's entitykey.

                    key= advWorksContext.CreateEntityKey("SalesOrderDetail", updatedItem);

// Get the original item based on the entitykey from the context

// or from the database.

if (advWorksContext.TryGetObjectByKey(key, out originalItem))

                    {

// Call the ApplyPropertyChanges method toapply changes

// from the updated item to the originalversion.

                        advWorksContext.ApplyPropertyChanges(

                            key.EntitySetName,updatedItem);

                    }

                    advWorksContext.SaveChanges();

                }

catch (InvalidOperationException ex)

                {

Console.WriteLine(ex.ToString());

                }

            }

        }

2.實現動態條件查詢。在本地環境中,對於Linq,我們可以通過動態構造Lambda表示式樹來實現動態條件查詢,但是在遠端環境中,Lamdba表示式不支援遠端序列化傳輸,只能通過ObjectContextCreateQuery方法實現,但幸好微軟後來又提供了一個LINQ動態查詢擴充套件庫Dynamic.cs,使用起來更方便,於是採用它實現。

3.EF中核心抽象類是ObjectContext,實體容器都從它派生,實體容器上的CUD方法其實都是通過呼叫ObjectContextCUD操作方法實現的。

1)AddObject(string,object):表示新增實體object到實體容器,只要實體的EntityKey值為空,無論是否Detached狀態均可以通過此方法實現新增操作。

2)ApplyPropertyChanges(string,object)表示把分離狀態的實體object上的所作的修改更新回容器中已存在的對應的實體,執行條件有兩個:①實體處於分離狀態,②實體容器中存在主鍵值與其相同的且為Unchanged狀態的實體,所以,當我們需要更新一個Detached狀態的實體時,可以先把一個具有原始值的相同鍵值的實體附加回容器中,或者直接執行一下查詢,從資料庫中取出該實體。

3)DeleteObject(object)表示從實體容器中刪除一個實體,執行條件是該實體存在於實體容器中,所以刪除一個Detach狀態的實體之前,需要把它通過Attach方法附加回實體容器中。

4.實體物件也是基於抽象類EntityObject派生的,由此我們完全可以用ContextObjectEntityObject實現服務端對實體的查詢和CUD方法,其實現子類在執行時由客戶端注入,從而使服務端和資料庫實現鬆耦合。

5.下圖是MSDN上關於在資料訪問層中使用 LINQ toSQL  n 層應用程式的基本體系結構圖,其實EF的結構也是一樣的,不過是把DataContext換成ObjectContext

四、動手開發

1.利用EF建立資料庫概念模型

新建一個解決方案EFServiceSystem,新增一個新專案,命名為EFModel,新增專案,在專案下新增一個ADO.NET EntityData Model項,命名為EFModel.edmx,選擇從資料庫生成(假設我們已經建好了一個SQL Server資料庫),一路點選下一步,直至完成。編譯專案成功後就算完成。為什麼要把資料庫模型單獨編譯成一個dll呢,我將在後面給予解釋。

2.建立資料服務層

在解決方案下再新增一個類庫專案,命名為EFService

1)利用外觀模式,我們把客戶端常用的查詢和CUD操作方法簡化為3個方法Query<T>,Save(Tt),Delete(T t),根據針對介面程式設計的設計原則,定義一個CUD方法介面供客戶端呼叫。

using System;

using System.Collections.Generic;

using System.Text;

namespace EFService

{

publicinterfaceIEntityHelper

    {  

List<T> Query<T>(string filter,paramsobject[] args);  

      TSave<T>(T t);

int Delete<T>(T t);      

    }

}

2)實現類EntityHelper的程式碼。主要思路是通過建構函式注入資料上下文例項名稱,在配置檔案取出其程式集限定名,通過反射建立例項,呼叫例項的相應方法實現介面。

using System;

using System.Collections.Generic;

using System.Text;

using System.Data.Objects;

using System.Data.Objects.DataClasses;

using System.Reflection;

using System.Linq;

using System.Linq.Dynamic;

using System.Runtime.Remoting;

namespace EFService

{

///<summary>

///為遠端客戶端提供實體查詢和CUD操作的服務類

///</summary>

publicclassEntityHelper : MarshalByRefObjectIEntityHelper

    {

///<summary>

///config檔案中配置的資料上下文名稱

///</summary>

string ContextName;

ServiceFactory factory = newServiceFactory();

public EntityHelper(string contextName)

        {

            ContextName= contextName;

        }

///<summary>

///建立資料上下文例項

///</summary>

///<returns></returns>

publicObjectContext CreateObjectContext()

        {

string typeName = System.Configuration.ConfigurationManager.AppSettings[ContextName].ToString();

return (ObjectContext)Activator.CreateInstance(Type.GetType(typeName));

        }

///<summary>

///查詢操作

///</summary>

///<typeparam name="T"></typeparam>

///<param name="filter">查詢條件組合</param>

///<param name="args">查詢條件中的引數值</param>

///<returns>返回實體集合</returns>

publicList<T> Query<T>(string filter, paramsobject[] args)

        {

using (ObjectContext context = CreateObjectContext())

            {

return (context.GetType().InvokeMember(

typeof(T).Name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty,null, context, null)

asIQueryable<T>).Where(filter, args).ToList();

            }

        }

///<summary>

///儲存實體

///</summary>

///<typeparamname="T"></typeparam>

///<param name="t">要新增或修改的實體</param>

///<returns>返回新增或者更新後的實體</returns>

public T Save<T>(T t)

        {

EntityObject eo = t asEntityObject;

using (ObjectContext context = CreateObjectContext())

            {

if (eo.EntityKey == null)

                {

                    context.AddObject(t.GetType().Name,t);

                }

else

                {

object target =ontext.GetObjectByKey(eo.EntityKey);                   context.ApplyPropertyChanges(eo.EntityKey.EntitySetName,eo);

                }

                context.SaveChanges();

return t;

            }

        }

///<summary>

///刪除實體

///</summary>

///<typeparamname="T"></typeparam>

///<paramname="t"></param>

///<returns>成功刪除的實體的數量</returns>

publicint Delete<T>(T t)

        {

EntityObject eo = t asEntityObject;

using (ObjectContext context = CreateObjectContext())

            {

if (eo != null && eo.EntityKey != null)

                    context.Attach(eo);

                context.DeleteObject(eo);

return context.SaveChanges();

            }

        }

    }

}

3)最後,我們建立一個服務工廠類,暴露給客戶端,負責以介面方式向客戶端提供遠端服務物件,資料服務層建立完畢。

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Data.Objects;

using System.Configuration;

namespace EFService

{

///<summary>

///遠端服務類工廠,負責建立服務物件

///</summary>

publicclassServiceFactory : MarshalByRefObject

    {

///<summary>

///建立遠端服務物件

///</summary>

///<paramname="connStringName">資料上下文名稱</param>

///<returns>遠端服務介面</returns>

publicIEntityHelper CreateEntityHelper(string contextName)

        {

returnnewEntityHelper(contextName);

        }

    }

}

3.建立執行服務的宿主程式。實際開發中,通常選擇建立一個windows服務程式來執行Remoting,但是服務需要安裝才能啟動,執行和除錯起來都比較繁瑣,所以這裡建立一個簡單的控制檯程式來執行它。在解決方案下新增一個控制檯程式專案,在program.cs編寫如下程式碼:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Tcp;

namespace EFServiceHost

{

classProgram

    {

staticvoid Main(string[] args)

        {

ChannelServices.RegisterChannel(newTcpChannel(9932),false);

RemotingConfiguration.ApplicationName = "EFService";           RemotingConfiguration.RegisterActivatedServiceType(typeof(EFService.ServiceFactory));           

Console.WriteLine("Start Server,Press any key toexit.");

Console.ReadLine();

        }

    }

}

配置檔案App.Config主要包括資料庫連線資訊以及自己定義一個數據上下文名稱(這裡和資料庫連線名稱相同,事實上不必相同),資料庫連線資訊可以從EFModel專案中配置檔案中直接拷貝過來。內容如下:

<?xmlversion="1.0"encoding="utf-8"?>

<configuration>

  <connectionStrings>

    <addname="SchoolEntities"connectionString="metadata=res://*/EFModel.csdl|res://*/EFModel.ssdl|res://*/EFModel.msl;provider=System.Data.SqlClient;providerconnection string=&quot;Data Source=192.168.0.110/SQLEXPRESS;Initial Catalog=School;IntegratedSecurity=True;MultipleActiveResultSets=True&quot;"providerName="System.Data.EntityClient"  >   

    </add

  </connectionStrings>

  <appSettings >

    <addkey="SchoolEntities"value="EFModel.SchoolEntities, EFModel, Version=1.0.0.0,Culture=neutral, PublicKeyToken=null" />

  </appSettings>

</configuration>

編譯成功後,拷貝EFModel和和EFService兩個專案生成的dll檔案至可執行檔案EFServiceHost.exe同一目錄下,點選執行EFServiceHost.exe。

4.最後,我們建立一個winform客戶端作為測試。在program.cs註冊遠端服務:

staticvoid Main()

        {

Application.EnableVisualStyles();

Application.SetCompatibleTextRenderingDefault(false);

//註冊遠端服務

RemotingConfiguration.RegisterActivatedClientType(

typeof(ServiceFactory), "tcp://localhost:9932/EFService");

Application.Run(newForm1());

        }

新增一個窗體Form1,放入一個按鈕,在按鈕點選處理事件里加入下面的程式碼,示範客戶端如何呼叫遠端服務類實現查詢和CUD操作,簡單起見,我不演示資料庫的資料變化了,反正,看程式碼,你懂的。

privatevoid button1_Click(object sender, EventArgs e)

        {

//通過遠端服務工廠創建出遠端服務物件,注意此處建構函式的參

注意此處參                         // 數值為遠端伺服器上配置檔案上的資料上下文名稱

ServiceFactory factory = newServiceFactory();         

IEntityHelper entityHelper =factory.CreateEntityHelper("SchoolEntities");

//查詢實體

List<Course> courses = entityHelper.Query<Course>("CourseID>@0",0);

Course course = courses.SingleOrDefault(p=> p.CourseID == 1045);

//修改實體

            course.Title= "updatesucceed";           

            entityHelper.Save(course);

//刪除實體

            entityHelper.Delete(course);

//新增實體

            entityHelper.Save(newCourse() { Title = "new course" });

     }

五、部署應用

1.至此,整個系統搭建完畢。在本例中,我把所有專案都統一建立在一個解決方案下,其實是為了演示方便,實際開發時候,完全可以各自獨立建立。下面我們來分析一下各個專案的職能和相互之間的引用關係。

1)EFModel:由Visual Studio 的資料模型工具生成的資料庫例項模型,提供資料的查詢以及CUD操作。不需引用其它專案。

2)EFService:使用資料庫例項模型以及實體的抽象基類編寫完成,程式碼裡不涉及具體資料庫模型例項,執行時通過客戶端注入引數和讀取配置檔案動態生成資料庫模型例項,並呼叫例項的查詢和CUD方法實現客戶端的請求。不需引用其它專案。

3)EFServiceHost:負責執行Remoting服務,如果通過配置檔案方式釋出服務的話,編譯時也不需引用其它專案,我這裡引用了EFService專案,是因為使用了程式碼方式暴露EFSservice的服務類。執行時需要將EFServiceEFModeldll檔案拷貝至執行目錄下。

4)EFClient:需要引用EFModelEFService(注:因為本例中式使用了Remoting作為遠端服務,如果是WebService或者WCF則只需新增服務引用,然後在本地生成客戶端代理類)。事實上EFService中的實現類EntityHelper也可以獨立出去,不必讓客戶端引用,對於客戶端而言,僅僅是使用ServiceFactory和介面IentityHelper就足夠了。這樣只要介面不變, EntityHelper更新的時候,客戶端無須更新引用,而且服務端程式碼可以完全被隔離開客戶端,對一些服務端和客戶端之間的保密性比較敏感的專案尤為有利。

2.通過分析我們發現,在開發下一個新專案的時候,即使整個資料庫都變了,從SQL SERVER變成Oracle,資料庫服務名變了,表也變了,我們仍然無需修改服務端程式碼,只需針對新的資料庫,生成新的EFModel,然後拷貝DLL檔案至EFServiceHost的執行目錄下(這也就是我為什麼要把EFModel獨立成一個專案的原因),再修改一下EFServiceHost的配置檔案中的資料庫連線和實體容器名稱即可完成新系統的部署。對於客戶端來說,也就是更新一下EFModel.dll,還是呼叫服務端提供的那幾個API,便可完成查詢和CUD操作,不用關心底層的資料庫是SQL Server還是Oracle,更不用自己實現對新庫新表的查詢和CUD操作(本來也不用)。當然,對於正在執行的系統,我們也可以針對新建資料庫生成新的實體模型DLL,拷貝至EFServiceHost執行目錄下,實現熱插拔方式擴充套件資料庫,而對原來的系統毫無影響,即使新加的庫是不同型別的庫。

六、系統架構圖示

七、總結

從以上分析可以看出,該系統運用到專案開發中,對服務端來說,實現了最大程度元件重用(零程式碼修改),對客戶端開發來說,高度簡化了對資料的操作命令,並封裝了實現細節,大大降低了開發的技術難度,提高了開發速度。當然,我這裡寫的程式碼僅僅是最簡單的演示程式碼,在實際專案開發中,服務端要處理的細節和擴充套件的功能要比這複雜得多。比如效能優化,實現複雜查詢和批量CUD操作,併發處理,事務控制,日誌跟蹤,資料快取等等。另外,如果各層採用不同的技術實現,服務層實現的程式碼也有差異。比如EF可以選擇最新版的更完善更強大的EF4.0,遠端服務可以選擇Remoting,WebService,WCF等,不同的遠端服務,宿主程式也有所不同,RemotingWCF可以選擇winform,控制檯程式,IIS,而Web Service只能選擇IIS。不同的服務,不同的宿主程式,會有不同的通訊通道 (Http,Tcp),不同的資料傳輸格式 (二進位制,XML,JSON)。如果你嫌上面的實現方式涉及的技術太多,開發起來太麻煩,那麼,微軟現成的具有REST風格的遠端資料服務WCF DataServices會是你的最佳選擇。WCF DataServices由於只用Http方式通訊,用XMLJSON格式傳輸資料,效能上會比用Tcp通訊二進位制格式傳輸的效能差一些,但是相信在將來基於SOA的分散式系統大行其道時,WCF DataServices會是.net實現SOA框架的主流技術,這裡基於篇幅所限,不作詳細介紹了,MSDN上有這方面的很詳盡的技術參考文件,有興趣的讀者可以自行參閱。