LINQ To SQL 與 N-Tier

這是我刊登於Run! PC上LINQ To SQL系列文章的其中之一,介紹LINQ To SQL在N-Tier的應用.

 

LINQ To SQL 與 N-Tier應用程式
 
/黃忠成
(原刊登於RUN! PC)
 
 從發表第一篇LINQ 首部曲到現在,轉眼間LINQ系列已來到第八篇了,這是我繼2000年IntraWeb以來,於Run! PC中連載次數最多的文章了。在這長達十個月的期間,我們由最基礎的LINQ To Objects,談到了LINQ To XML及LINQ To DataSet的擴充應用,來到了LINQ To SQL的ORM世界,最後將於LINQ To SQL 的實務應用中劃下句點。
 當初,LINQ之所以會吸引我目光的原因有二,一是其與語言整合的奇想,讓我覺得相當的新鮮且有創意,二則是ORM的誘惑,多年來,我一直對ORM有著相當高的興趣,從研究ORM概念到使用ORM產品,乃至於自行開發ORM Framework,對於這個技術,我有著深入的研究及無限的渴望。
做為Microsoft第一個公開的ORM實作體,LINQ To SQL不管是於效率還是易用性上,都有著不俗的表現,但在N-Tier應用上,LINQ To SQL卻一直處於相當不明確的狀態,
即使到了Visual Studio 2008正式上市的今天,仍然沒有一個完整的文件及範例,告訴我們如何將LINQ To SQL應用於N-Tier系統架構中,本系列文章將以一個小型的N-Tier範例做為結尾,為讀者們演示,如何將LINQ To SQL應用於以WPF做為UI層的N-Tier系統架構上。
 
LINQ To SQL的N-Tier應用
 
 在開始討論LINQ To SQL與N-Tier之前,我們得先定位出什麼是N-Tier應用程式,傳統的N-Tier應用程式架構如圖1所示。
圖1
說穿了,N-Tier系統架構就是於傳統的Client/Server架構中添加一台架設應用程式伺服器的電腦,做為橋接Client端與Server端(也就是架設資料庫系統的那台電腦)的中介者。那為何要這麼做呢?主要原因有兩個,一是擴充性的考量:在傳統的Client/Server架構中,Server端必須負擔服務所有客戶端的重責大任,一旦客戶端增多,Server端電腦的等級也必須隨之提升,否則效能會因大量的客戶端而降低,架設中介伺服器可以分擔原本由Server端一手包辦的工作。
在面對大量使用者同時操作系統的情況下,中介伺服器可以快取部份常用的資料,以暫存方式來取代真正的資料庫操作,進而減輕Server端的負擔。在N-Tier架構中,中介伺服器並不僅限於一台電腦,這意味著我們可以架設多台中介伺服器,然後讓Client端連往主控的中介伺服器,再由此中介伺服器決定該Client端是要連往那一台中介伺服器,這也是N-Tier用語中常見的『負載平衡』。
二是安全性的考量:原本在Client/Server架構中,Client端是直接與Server端溝通的,這意味著如果應用程式需要透過網際網路來連結Server時,該Server就必須曝露於網際網路上才行,但這個Server端通常是一個架設資料庫系統的電腦,將整個公司重要的資料曝露於眾人可及的網路上,相信沒有幾家公司有這個勇氣或膽識吧。
藉助於中介伺服器的架設,可讓後端資料庫免除於曝露於網路上的危機。而Client端與中介伺服器間的溝通立基於有限的通訊規格,所以即使中介伺服器需曝露於網際網路上,有心人士透過 中介伺服器來傷害後端資料庫的機會及層次也降低了不少,何況通訊加密是所有N-Tier應用程式必須實作的部份,比起簡單的資料庫通訊協定,專為N-Tier應用程式所設計的安全通訊協定如Web Services,自然安全許多。
 
 
.NET Framework 3.5下的N-Tier
 
 當決定採用N-Tier架構時,Client端與中介伺服器間的通訊協定便成了第一個得做出的選擇,從.NET Framework 3.0開始,Microsoft便致力於打造新一代可用於N-Tier架構的通訊協定:Windows Communication Foundation,簡稱WCF。在原始的設計概念中,WCF必須解決兩個問題,一是效率的問題,以往的N-Tier程式可分成兩大陣營,一是使用公開的標準規格:Web Services做為通訊協定,二是使用封閉的規格:Remoting。由於可使用Binary(二進位)協定來封裝訊息,所以Remoting在效能上有著相當耀眼的表現,但Remoting是封閉的協定,所以失去了與其它平台互通的可能性。使用Web Services雖然可以得到了平台互通的特色,但卻必須以效率做為交換條件。
WCF在訊息封裝層做了一個抽象化的設計,可以讓WCF應用程式於執行時期切換使用的通訊協定,這也就是說,當在網路環境良好,且沒有平台互通性考量的情況下,我們可以在不重新編譯應用程式的狀態下,僅修改組態檔就能將架構於WCF的應用程式之通訊層換成Binary格式,當需要互通性時,也只要修改組態檔就能改為Web Services格式,此項設計讓WCF應用程式可以輕鬆遊走於兩種通訊協定間,不像以往般,一旦選擇了Remoting後,要改成Web Services就很難了。
WCF出現的第二個目的是要實作更多的Web Services規格,這幾年來Web Services的規格鋪天蓋地的出現,能支援更多的規格代表著與其它平台的互通性也就越高,WCF中支援了如WS-Security、WS-ReliableMessage、WS-Coordation、WS-Address等新規格。因此,在.NET Framework 3.5中,不管是依據效能或是互通性的考量,WCF都是最好的選擇。
圖2 .NET Framework 3.5的N-Tier架構
 
 
Entity Object的序列化
 
 談完了系統架構後,現在讓我們回到主軸上,如何將LINQ To SQL應用於N-Tier應用程式架構中,在這個階段首要必須解決的問題是,LINQ To SQL的Entity Object能否透過Web Service、Remoting、WCF來傳遞?
答案是肯定的,不支援這個的話,LINQ To SQL就幾乎沒有存在的價值了。不過這有個小技巧,在預設設定中,LINQ To SQL Designer所產生出來的Entity Class是不支援序列化的,你必須在LINQ To SQL Designer中調整Serialization Mode屬性來要求其產生可序列化的Entity Class。
圖3
在設定Serialization Mode為Unidirectional後,所產生的Entity Class便會標上傳統物件序列化所須的[Serializable]及WCF所須的[DataContrast]等兩個Attribute,有了這兩個Attribute後,Entity Object便可傳遞於Remoting、Web Services、WCF等通訊協定。
 
中介伺服器端的實作
 
 用WCF來實作中介伺服器是一件相當輕鬆的事,首先請建立一個WCF Service Application專案。
圖4
然後添加一個LINQ To SQL Classes項目於此專案中,本例於此添加了北風資料庫的Customers資料表。
圖5
接著刪除自動產生的IService1.cs,於Service1.svc.cs中鍵入程式1的程式碼。

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
 
namespace WCFDataService
{
    [ServiceContract]
    public interface IDataService
    {
        [OperationContract]
        Customer[] GetAllData();
    }
 
    public class DataService : IDataService
    {
        #region IDataService Members
 
        public Customer[] GetAllData()
        {
            NorthwindDataContext context = new NorthwindDataContext();
            return (from s1 in context.Customers select s1).ToArray();
        }
        #endregion
    }
}
最後調整app.config中關於Service的設定後,便完成了此一執行於中介伺服器上的WCF Service的建構工作。
 
WPF客戶端的實作
 
 那如何在WPF客戶端使用這個WCF Service呢?如同以往使用Web Service般,我們可以透過Visual Studio 2008來引用此WCF Service。
圖6
就像以往的Add Web Reference功能一樣,此精靈會引導我們取得WCF Service,然後產生出Proxy Class。
圖7
取得Proxy Class後,接下來就是建構UI的工作了,這裡我使用與前幾期WPF同樣的UI畫面,稍加調整程式碼,透過WCF Service來取得資料。
程式2

 

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    var result = (from s1 in client.GetAllData() orderby s1.CustomerID select s1).ToList();
   bindingList = new BindingList<DataService.Customer>(
(IList<DataService.Customer>)result);
    DataContext = bindingList;
    view = CollectionViewSource.GetDefaultView(DataContext);
}
執行結果如圖8。
圖8
 
 
 
Tracking Changes機制?
 
 OK,我們能夠順利的由WCF Service取得資料,那麼接下來的更新動作該如何做呢?,照 MSDN中『順帶一提』的說明中,要於N-Tier情況下實作更新功能必須視UI層而定,於ASP.NET中可透過ObjectDataSource控件的協助完成,但在WPF、Windows Form中,程式設計師則必須自行實作Client端的Tracking Changes機制
 那什麼是Tracking Changes機制呢?簡單的說,更新一筆資料需要有兩個資訊,一是Entity Object的現值,一則是Entity Object的原始值,在更新資料時,我們必須將這兩個資訊送達WCF Service,然後由WCF Service依據原值來取得欲更新的資料列後,將現值更新進去。
 問題在,LINQ To SQL Designer只是將Entity Class標示為可序列化,並未產生出任何的Tracking Changes所需要的程式碼,這也就是說!在WPF端時,我們必須於資料列更新時,將原值先記錄下來,否則就無法透過WCF Service來更新該筆資料了。更確切的說,就是少了一個類似DataSet之GetChanges函式的機制。
 那該如何實作這個機制呢?很幸運的,LINQ To SQL Designer所產生出的Entity Class實作了INotifyPropertyChanging介面,因此我們可掛載事件至其所定義的PropertyChanging事件中,於物件屬性值改變時,事先將原值記錄下來,完成Tracking Changes機制。
但也很不幸的,透過WCF Service所產生的Proxy Class忽略了此介面,並沒有產生出對應的程式碼,所以透過INotifyPropertyChanging介面來實作Tracking Changes的想法是不可能達到的。
 退而求其次,我們只能以Context的概念來實作Tracking Changes,也就是說於取得物件的同時,將所有物件複製一份,將原值保留下來,然後透過另一介面INotifyPropertyChanged來偵測物件是否已被改變。
程式3

 

using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
 
namespace WpfDataConsumer
{
    public class TrackingContext<T,TUpdate>
    {
        …………….
        public void Initialize(IList<T> objs)
        {
            _states.Clear();
            _update_original = typeof(TUpdate).GetProperty("Original");
            _update_current = typeof(TUpdate).GetProperty("Current");
            _update_state = typeof(TUpdate).GetProperty("State");
            foreach (T item in objs)
            {
                object updateData = Activator.CreateInstance(typeof(TUpdate), false);
                _update_original.SetValue(updateData, CloneObject(item),null);
                _update_current.SetValue(updateData, item, null);
                _update_state.SetValue(updateData,
WpfDataConsumer.DataService.UpdateState.UnChanged, null);
                ((INotifyPropertyChanged)item).PropertyChanged +=
new PropertyChangedEventHandler(TrackingContext_PropertyChanged);
                _states.Add(item, (TUpdate)updateData);
            }            
        }
 
void TrackingContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (_states.ContainsKey((T)sender))
            {
                object o = _states[(T)sender];
                DataService.UpdateState state =
(DataService.UpdateState)_update_state.GetValue(o, null);
                if (state == WpfDataConsumer.DataService.UpdateState.Insert)
                    return;
                _update_state.SetValue(o,
WpfDataConsumer.DataService.UpdateState.Update, null);
            }
        }
}
}
 
 
實作Server端的Update機制
 
 有了Tracking Changes機制的協助後,WCF Service端可以獲得欲更新資料物件的原值與現值,要更新資料就不難了。
程式4

 

public void UpdateData(UpdateData<Customer>[] updatedData)
{
      NorthwindDataContext context = new NorthwindDataContext();
      try
      {
         foreach (UpdateData<Customer> item in updatedData)
         {
             if (item.State == UpdateState.Insert)
                 context.Customers.InsertOnSubmit((Customer)item.Current);
         }
 
         foreach (UpdateData<Customer> item in updatedData)
        {
            if (item.State == UpdateState.Update)
                 context.Customers.Attach((Customer)item.Current, (Customer)item.Original);
         }
 
         foreach (UpdateData<Customer> item in updatedData)
         {
            if (item.State == UpdateState.Delete)
            {
                 context.Customers.Attach((Customer)item.Current);
                 context.Customers.DeleteOnSubmit((Customer)item.Current);
             }
          }
          context.SubmitChanges();
       }
       finally
       {
           context.Dispose();
       }
 }
 
 
下一期,ADO.NET Entity Framework
 
 在這一期文章中,我給了各位讀者們一個於LINQ To SQL中使用N-Tier架構,並以WPF做為UI層的範例,這個範例的程式碼不多,但是其中的概念卻是相當的複雜,在這個範例中應用到了以Context概念實作Tracking Changes,以泛型作為讓Trancking Changes可以適用於多類型物件的解法,要想於一篇文章中將其完全解釋是不可能的,如果讀者們對此種架構有需求或是興趣,請下載範例後以除錯模式觀察,必能得箇中巧妙之處。
 這也是LINQ系列的最後一篇文章,全系列的LINQ文章完整版多數已收錄於我在四月即將出版的新書中,下一期我們將跳出LINQ的框框,與讀者們一起討論Microsoft另一個ORM技術:ADO.NET Entity Framework,別誤會!我們依然還在LINQ的勢力範圍裡,因為!ADO.NET Entity Framework依然支援以LINQ來查詢物件,也就是說你還是會見到LINQ啦,下期再見了。
 
參考書目:
 
   次世代.NET資料庫開發聖典 ASP.NET黃忠成(四月份出版)
   Microsoft Windows Communication Foundation 新一代應用程式通訊架構李漢宗
 
範例下載: