當轉為使用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);
//==========================================================
//-- Open the Connection and create the Transaction
//==========================================================
oCn.Open();
oTrn = oCn.BeginTransaction();
//==========================================================
//-- Set up the INSERT Command
//==========================================================
sProcName = "prInsert_Order";
oInsCmd = new SqlCommand(sProcName, oCn, oTrn);
oInsCmd.CommandType = CommandType.StoredProcedure;
oInsCmd.Parameters.Add(new SqlParameter("@sCustomerID", SqlDbType.NChar, 5, "CustomerID"));
oInsCmd.Parameters.Add(new SqlParameter("@dtOrderDate",
SqlDbType.DateTime, 8,"OrderDate"));
oInsCmd.Parameters.Add(new SqlParameter("@sShipCity",
SqlDbType.NVarChar, 30, "ShipCity"));
oInsCmd.Parameters.Add(new SqlParameter("@sShipCountry", SqlDbType.NVarChar, 30, "ShipCountry"));
oDa.InsertCommand = oInsCmd;
//==========================================================
//-- Set up the UPDATE Command
//==========================================================
sProcName = "prUpdate_Order";
oUpdCmd = new SqlCommand(sProcName, oCn, oTrn);
oUpdCmd.CommandType = CommandType.StoredProcedure;
oUpdCmd.Parameters.Add(new SqlParameter("@nOrderID", SqlDbType.Int, 4, "OrderID"));
oUpdCmd.Parameters.Add(new SqlParameter("@dtOrderDate", SqlDbType.DateTime, 8,"OrderDate"));
oUpdCmd.Parameters.Add(new SqlParameter("@sShipCity", SqlDbType.NVarChar, 30, "ShipCity"));
oUpdCmd.Parameters.Add(new SqlParameter("@sShipCountry", SqlDbType.NVarChar, 30, "ShipCountry"));
oDa.UpdateCommand = oUpdCmd;
//==========================================================
//-- Set up the DELETE Command
//==========================================================
sProcName = "prDelete_Order";
oDelCmd = new SqlCommand(sProcName, oCn, oTrn);
oDelCmd.CommandType = CommandType.StoredProcedure;
oDelCmd.Parameters.Add(new SqlParameter("@nOrderID", SqlDbType.Int, 4, "OrderID"));
oDa.DeleteCommand = oDelCmd;
//==========================================================
//-- Save all changes to the database
//==========================================================
oDa.Update(oDs.Tables["Orders"]);
oTrn.Commit();
oCn.Close();
}
catch (DBConcurrencyException exDBConcurrency)
{
//=======================================================
//-- Roll back the transaction
//=======================================================
oTrn.Rollback();
//--------------------------------------------------------
//-- May want to rethrow the Exception at this point.
//-- This depends on how you want to handle the concurrency
//-- issue.
//--------------------------------------------------------
//-- throw(exDBConcurrency);
}
catch (Exception ex)
{
//==========================================================
//-- Roll back the transaction
//==========================================================
oTrn.Rollback();
//--------------------------------------------------------
//-- Rethrow the Exception
//--------------------------------------------------------
throw;
}
finally
{
oInsCmd.Dispose();
oUpdCmd.Dispose();
oDelCmd.Dispose();
oDa.Dispose();
oTrn.Dispose();
oCn.Dispose();
}
oCn.Close();
return oDs;
}
圖2 捕獲ADO.NET中的並發性問題
在圖2 的代碼中,可以注意到一個示例方法,它接收一個DataSet參數,並將DataSet中的更改通過DataAdapter的Update方法提交給數據庫。如果檢測到並發性問題,將由一個特定的catch代碼塊引發和捕獲DBConcurrencyException異常。此時,事務可以回滾,然後還可重新引發異常,直到最終異常被客戶端應用程序捕獲,從而得以通知用戶並詢問用戶希望如何處理。這裡同樣要仔細地考慮catch代碼塊的順序。例如,如果首先出現一般性的catch代碼塊,那麼它已經捕獲了並發性問題。異常處理的關鍵在於,要確保針對更特定異常的catch代碼塊在其他catch代碼塊之前出現,從而可由特定catch代碼塊首先捕獲特定異常。
批量更新
ADO Recordset對象經常會用到的一種情形是進行批量更新。例如,在一個N層應用程序中,ADO Recordset可檢索一個行集,將其從數據源中斷開連接,並將Recordset發送到客戶端層。在客戶端應用程序中,對幾行的更改將在斷開連接的ADO Recordset中進行。然後Recordset可被發回給中間層,通過ADO Connection對象與數據庫重新連接,其更改可通過UpdateBatch方法應用於數據庫。UpdateBatch方法將接受Recordset中的所有被更改的行,並將更改應用於Recordset源查詢中指示的源表。ADO.NET也有內置的處理批量更新的功能。DataSet是一個自成一體的、總是斷開連接的行集集合,它可以存儲行集中行與列的原始值和當前值。DataSet可以被發送給客戶端應用程序,由後者進行更改操作。DataSet然後被發送到中間層,其更改將通過DataAdapter對象應用於數據庫。圖2說明了ADO.NET DataAdapter如何使用其不同的Command對象執行SELECT、INSERT、UPDATE和DELETE SQL命令:
//-- Setting the ADO.NET DataAdapter's command objects
oDa.InsertCommand = oInsCmd;
oDa.UpdateCommand = oUpdCmd;
oDa.DeleteCommand = oDelCmd;
oDa.Update(oDs.Tables["Orders"]);
ADO.NET技術在對數據庫應用更新的方式上比傳統的ADO批量更新技術更靈活。傳統ADO是通過查看ADO Recordset的Source屬性來確定保存更改的位置。例如,假定Recordset的源是:
SELECT OrderID, CustomerID, OrderDate, ShipCity, ShipCountry FROM Orders
這種情況下,在對Recordset已經進行批量更改而且調用了Recordset的UpdateBatch方法之後,Recordset必須指向發送更改的位置。它查看源的SQL語句,並確定Orders表的主鍵(OrderID)在語句中而且只用到了一個表。因此,它可以使用SQL語句創建UPDATE、INSERT和DELETE語句對Orders表進行處理。為了能夠派生操作查詢,Recordset必須知道唯一行標識符。同樣,存儲過程不能用於這種技術更新數據庫,因為語句是隱式創建的。以下ADO 2.x代碼說明了包含Orders的Recordset如何在客戶端應用程序中進行更新:
'-- Using traditional ADO,
'-- Updating two rows in the client tier
oRs.Find "CustomerID = 'VINET'"
oRs.Fields("ShipCity") = "Somewhere"
oRs.Update
oRs.Find "CustomerID = 'CHOPS'"
oRs.Fields("ShipCity") = "Elsewhere"
oRs.Update
之後,一旦Recordset傳回給中間層,將調用以下代碼從而將已完成的更改應用於數據庫:
'-- Using traditional ADO,
'-- Sending both updated rows to the database, in the business tier
oRs.UpdateBatch
ADO.NET CommandBuilder的工作方式與此類似,因為它可以自動地為DataAdapter生成UpdateCommand、InsertCommand和DeleteCommand對象。自動生成命令的過程也會帶來系統開銷。顯式地指定要使用的INSERT、UPDATE和DELETE語句效率更高。與顯式指定命令相比,使用ADO.NET CommandBuilder意味著性能更低下,在對數據源應用更改方式上的控制也更差。准確的SELECT、INSERT、UPDATE和DELETE語句通常在設計時是已知的,可以用來生成ADO.NET DataAdapter的四個不同Command屬性,它們甚至可以用存儲過程的名稱表示。ADO.NET的這一功能是傳統的ADO所缺乏的一個關鍵部分,這可以大大提高傳統ADO的批量更新技術的靈活性。當然,這需要在設計時進行更多的編碼工作,但為了獲得它所提供的更大靈活性(可使您非常具體地指定SQL語句或更常見地用於為每個操作指定存儲過程),付出這些努力也是值得的。
斷開連接的行集
通過適當設置一些屬性,ADO Recordset對象可以從其數據源中斷開連接。通過斷開連接,它能夠在內存中存儲整個行集,並在使用傳統ADO的應用程序之間傳遞。以下代碼示例將斷開Recordset的連接:
'-- Disconnecting an ADO Recordset
oRs.CursorLocation = adUseClient
oRs.CursorType = adOpenStatic
oRs.LockType = adLockBatchOptimistic
oRs.Open
Set oRs.ActiveConnection = Nothing
斷開Recordset的連接涉及的關鍵屬性是CursorType和CursorLocation。CursorLocation必須設置為adUseClient,表示行集應該存儲在Recordset的內存中。CursorType應該設置為adOpenStatic,這將允許行集的游標能夠在任何方向上移動,但是不允許行集自動對基礎數據庫的更改處於敏感狀態。通過結合使用這兩個設置,Recordset可以斷開連接,但是,全部步驟還沒有完。一旦打開Recordset,因為它真的斷開連接了,那麼它的ActiveConnection屬性應該被設置為關鍵字Nothing,就像上一個代碼示例中看到的那樣。構建ADO Recordset是為了在斷開連接和已連接這兩種模式下工作。但是在ADO.NET中,當DataSet完全斷開連接時,DataReader要維持連接。DataSet是斷開連接的ADO Recordset的繼承者,因為它實現了一個客戶端游標,可以向任何方向滾動,而且ADO.NET DataSet還支持許多其他功能。傳統的ADO Recordset對象提供的斷開連接功能非常有限,而ADO.NET DataSet對象是專門為斷開連接而設計的。與Recordset不同,DataSet可以在DataTable對象內存儲和表示多個行集,甚至還可以使用DataRelation對象將它們互相聯系起來。下面是使用SqlDataAdapter的Fill方法創建和填充DataSet的一種常見方式:
//-- Creating and filling an ADO.NET DataSet
DataSet oDs = new DataSet("MyDataSet");
oDa.Fill(oDs);
DataSet可以施加關系、主鍵約束和外鍵約束,甚至實現列表達式。ADO Recordset還能夠使用它的數據構形功能返回層次化的結果。雖然有這些層次化功能,但是它們需要使用我個人認為非常笨拙的數據構形語法。就此方面而言,ADO.NET DataSet更加高效也更加直觀,因為它就是為使用其DataRelation和DataTable對象處理關系數據結構而構建的。
在之前,我討論了幾個傳統的ADO 2.x開發中常用的技術,以及如何將它們轉換到ADO.NET。ADO.NET是ADO 2.x發展的下一階段,許多傳統ADO中的功能都將在ADO.NET中進行全面改造。例如,ADO 2.x中添加了Open和Save方法的功能以支持XML,而XML支持從一開始就已經被納入到ADO.NET的設計藍圖中了。ADO的一些功能發生了很大變化,變化之一就是Recordset的許多游標類型被分為多個不同的各有側重的對象,例如DataReader、DataSet和DataAdapter。其他功能(例如Connection和Command類)的改變則不是那麼明顯。但是,說到底,從ADO到ADO.NET的遷移還是相對比較易懂的,這是因為ADO.NET的設計構築於傳統ADO的使用體驗之上。