當轉為使用ADO.NET時,您將需要了解如何應對以前知道用ADO處理而現在必須用ADO.NET解決的場景。就像使用Visual Basic、C++和ASP開發的N層解決方案經常要依賴ADO來滿足數據訪問需要一樣,Windows?窗體、Web窗體和Web服務也要依賴ADO.NET。我曾經從使用傳統ADO開發的角度討論了如何使用ADO.NET來處理一些數據訪問的場景。其中的一些主題包括將行集保留為XML、處理只進流水游標和執行Command對象的多種方式。在文中,我將繼續討論使用ADO.NET的開發場景,以及使用傳統的ADO技術是如何處理的。我將從經常使用的傳統ADO的只進、靜態、鍵集和動態游標的情況開始討論。我還要討論如何處理並發性問題,以及斷開連接的行集如何從ADO演變到ADO.NET。然後,我會說明如何將使用傳統的ADO處理批量更新的代碼轉換為使用ADO.NET(用DataAdapter及其四個命令對象)的代碼。
分散Recordset的功能
在ADO.NET中,ADO Recordset的大多數功能都分到了三個主要對象中:DataReader、DataSet和DataAdapter(參見圖1)。
圖1 ADO Recordset
ADO.NET DataReader對象被設計成服務器端的只進、只讀游標。ADO.NET DataSet對象是行集斷開連接的存儲工具。它存儲記錄,但不持有與數據源的連接,事實上,它並不關心其行集從哪個數據源派生。DataSet在內存中存儲時是一個二進制對象,但是它可以輕松地從XML序列化和序列化為XML。這與ADO Recordset對象從其相關聯的Connection對象斷開連接(通過Recordset的ActiveConnection屬性)時將CursorType設置為adOpenStatic、將CursorLocation設置為adUseClient是類似的。ADO.NET DataAdapter對象是連接與DataSet對象之間的橋梁,它可以通過一個連接從數據源加載一個DataSet,並用DataSet中存儲的已更改的內容更新數據源。ADO Recordset的行為取決於其屬性的設置(包括CursorType和CursorLocation屬性)。在ADO.NET中構建了不同的對象來處理這些特定的情況,而無需用一個對象來應對所有情況。
流水游標
傳統的ADO公開了四個不同類型的游標,可以改變ADO Recordset對象的運作方式。ADO Recordset對象的行為方式因其CursorType屬性的設置情況互不相同,可能有非常大的差異。例如,通過將CursorType設置為adOpenForwardOnly,Recordset可保持與其數據源的連接,而且必須按只進方向進行遍歷。然而,當您將CursorType屬性設置為adOpenDynamic時,Recordset可向前或者向後遍歷,甚至可以使游標跳到某個特定行。通過其CursorType和CursorLocation屬性,ADO Recordset對象采用了一種將許多解決方案包裝到單個對象中的方式。而ADO.NET采用的方法則不相同,它設計由不同的對象和方法來處理各種特定情況。在傳統的ADO中,只進、只讀游標是通過將CursorType設置為adOpenForwardOnly、將CursorLocation設置為adUseServer(這也是默認設置)實現的,這將使Recordset對象采用只進的服務器端游標形式。MoveNext方法將Recordset重定位到下一行,而MovePrevious方法則根本不允許使用,但是可以調用Recordset的MoveFirst方法。這有些容易引起誤解,因為該方法並不將Recordset重定位到當前行集的開始;相反,它調用原來的SQL語句,從頭開始重新填充Recordset,因此會再次移動到第一個記錄。每次執行傳統ADO Recordset的MoveFirst方法時(CursorType設置為adOpenForwardOnly),通過打開SQL事件探查器工具,觀察SQL的執行情況,可以很容易地看到這一點。在需要逐個遍歷成千上萬(乃至更多)行中的每行或者在需要小一些的行集但是只需遍歷一次(可能為了加載到一個選取列表)時,這種游標是非常常用的:
-- Forward-only Firehose Cursor in ASP and ADO
Set oRs.ActiveConnection = oCn
oRs.LockType = adLockReadOnly
oRs.CursorType = adOpenForwardOnly
oRs.CursorLocation = adUseServer
oRs.Open sSQL
最接近這種傳統的ADO流水游標的等效物是ADO.NET DataReader對象。和傳統的只進ADO Recordset一樣,DataReader在打開的同時保持著與其數據源的連接,而且只能以前進的方向進行遍歷。但是,它們還是有區別的。區別之一是,專門為某個data provider(如SQL Server?(SqlDataReader類)或者ODBC數據源(OdbcDataReader類))編寫單獨的DataReader類型。ADO.NET DataReader對象非常高效,這是因為它是專門為實現只進、只讀游標這一目的而構建的。傳統ADO的只進游標是通過相同的Recordset對象(作為一個斷開連接的Recordset甚至是一個數據源敏感的Recordset)實現的。而DataReader只是為了作為輕型流水游標而設計的。
//-- ASP.NET and ADO.NET in C#
SqlDataReader oDr = oCmd.ExecuteReader();
數據敏感游標
傳統ADO的只進游標現在由DataReader對象處理。但是,對基礎數據庫中的更改非常敏感的鍵集和動態服務器端游標(CursorType = adOpenKeySet和CursorType = adOpenDynamic)又怎麼樣呢?當前版本的ADO.NET並沒有公開多方向、可滾動、可更新的服務器端游標。但是,ADO.NET確實提供了許多方式避免使用這些類型的服務器端游標,因為有其他推薦的技術可以考慮。除了使用DataSet結合DataAdapter來檢索和更新數據以外,還有一些好的替代方案在客戶端使用可滾動的服務器端游標。例如,可以使用存儲過程,在運行服務器端SQL處理時這是非常高效的。還可以通過服務器端只進游標用DataReader檢索數據,然後用Command對象分別進行更新。但是,經常有比使用可更新的服務器端游標更高效的修改數據的方式。
在一個N層環境中,服務器端可滾動游標的問題之一在於,它們需要在服務器上保持狀態。因此,如果應用程序在中間層使用服務器端可滾動游標,客戶端層就需要維持與可滾動游標所在的業務層的連接。這樣,在同樣使用可滾動游標的客戶端應用程序屏幕存在期間,業務對象都需要保留。各種文檔中已經很好地記錄了這些可伸縮性問題,但是,在一些情況下服務器端游標還是很有價值的,如需要只進流水游標的情況。例如,在應用程序需要注重數據並發性問題的時候就要用到服務器端游標。使用傳統的ADO,Recordset可以用一個動態游標打開,這種游標使Recordset能夠獲悉所有其他用戶進行的插入、更改和刪除操作。這種服務器端游標對於其他用戶所做的更改非常敏感,可以在應用程序出現並發性問題的時候用於采取主動措施。例如,傳統ADO中的動態游標經常用於這樣的場合:應用程序准備在數據庫中保存更改,但是又需要確保首先知道是否有其他人更改了同一條記錄。當用戶更改動態Recordset中一個記錄值的時候,該值將自動在服務器端動態Recordset中更新:
-- Dynamic Cursor in ASP and ADO
Set oRs.ActiveConnection = oCn
oRs.LockType = adLockOptimistic
oRs.CursorType = adOpenDynamic
oRs.CursorLocation = adUseServer
oRs.Open sSQL
服務器端游標與並發性
並發性問題在許多企業級應用程序中是合理而且常見的問題,但是使用服務器端游標以解決這一問題的代價卻可能非常之高。那麼在ADO.NET中有什麼替代方式處理並發性問題呢?常見的技術是允許應用程序對數據庫提交更改,然後在遇到並發性問題時引發特殊類型的異常。有一個特殊類型的異常名為DBConcurrencyException,它是從Exception基類派生而來的。因為DataSet將每列的原始值和擬更改值存儲到一行中,它知道如何將值與數據庫進行比較以自動尋找並發性問題。例如,假定用戶將firstname字段的值由Lloyd更改為Lamar,並將更改提交給數據庫,存儲在DataSet中的更改將通過DataAdapter的Update方法傳遞給數據庫。在數據真正保存之前,如果數據庫中的名字不再是Lloyd,現在變成了Lorenzo,將會引發一個DBConcurrencyException異常。此異常可以以最適合應用程序需要的任何方式進行捕獲和處理,例如,可以采取“最後者優先”的規則,使用戶可以選擇取消更改、改寫或者強制更新或其他技術。
public DataSet SaveData(DataSet oDs)
{
string sMethodName = "[public void SaveData(DataSet oDs)]";
//==========================================================
//-- Establish local variables
//==========================================================
string sProcName;
string sConnString = "Server=(local);Database=Northwind;Integrated
Security=SSPI";
SqlDataAdapter oDa = new SqlDataAdapter();
SqlTransaction oTrn = null;
SqlConnection oCn = null;
SqlCommand oInsCmd = null;
SqlCommand oUpdCmd = null;
SqlCommand oDelCmd = null;
try
{
//==========================================================
//-- Set up the Connection
//==========================================================
oCn = new SqlConnection(sConnString);
//======================