ADO.NET 2.0 資料存取 ADO.NET 1.x 是 ActiveX Data Objects 2.6(ADO) 的後繼者 ADO.NET 1.x 最主 要的目標, 是讓使用者可以輕鬆地利用.NET Framework 建立分散式的資料共享應 用程式 ADO.NET 2.0 的主要目標則是改進 ADO.NET 1.x 版中既有功能的效率, 讓使用過程變得更簡單, 並且在不破壞回溯相容性的情況下增加新的功能 在本章中, 如果提到 ADO.NET 的時候並沒有加上後續的版本號碼 ( 也就是 1.x 或 2.0), 則表示該段陳述同時適用於 ADO.NET 的所有版本 ADO.NET 係建構在 XML 等業界標準上, 而且和 ADO 一樣提供了與 SQL Server 或 Oracle 等資料來源進行溝通的資料存取介面 應用程式可以利用 ADO.NET 連 線至這些資料來源, 然後進行資料的取得, 處理以及更新等動作 雖然 ADO.NET 2.0 增加了一些新功能, 但並沒有破壞任何與 ADO.NET 1.x 之間的相容性 在需要進行離線或遠端存取資料的解決方案中,ADO.NET 2.0 都是使用 XML 來與 程式或網頁交換資料 只要是能夠讀取 XML 的元件, 就可以使用 ADO.NET 元 件 如果傳送端的 ADO.NET 元件可以將資料包裝成 XML 檔案以進行傳遞, 接收 元件便不一定得要是 ADO.NET 元件 利用 XML 檔案格式的資料集合來傳送資 訊, 可以讓設計者輕鬆地將資料共享應用程式的資料處理元件及使用者介面分開放置在獨立的伺服器上面 這種作法可以大幅改善必須支援多使用者的系統所擁有的效能與可維護性 就分散式應用程式而言, ADO.NET 1.x 證明了使用 XML 資料集合可以比 ADO 中用來傳輸離線資料集合的 COM 封送處理 (marshaling) 提供更高的效能 由於資料集合的傳輸是採用依循簡易文字標準的 XML 資料流, 因此在接收元件時完全沒有 COM 所需的結構性限制 ADO.NET 1.x 所使用的 XML 資料集合也省略了將
第十一章 Recordset 的 Fields 集合轉換為 COM 能夠辨識的資料型別時所需的處理成本 基本上, 只要分屬不同系統的兩個元件在處理資料集合的格式時使用的是相同的 XML 結構描述 (schema), 就可以共享資料集合 XML 的整合在 ADO.NET 2.0 中依舊強悍, 而在 DataSet 物件的效能上也有著相當廣泛的進步, 尤其是在序列化及記憶體使用等方面 ADO.NET 也支援了以網頁為基礎的資料共享程式在規模上的可調整性 Web 應用程式常需要為成千上百位使用者提供服務 根據預設值,ADO.NET 並不會保留佔據資源的資料庫鎖定檔案, 或是作用中的資料庫連線 因此, 就算使用者的人數增加, 對於系統資源的需求也只會呈現小幅度的成長 在這一章中, 您將會發現 ADO.NET 2.0 是一套廣泛且具有彈性的 API, 可以用來存取很多種的資料 另外, 由於 ADO.NET 2.0 是 ADO.NET 1.x 的擴充版本, 您可以運用相當多 ADO.NET 的既有知識 實際上, 如果您對於 ADO.NET 1.x 及.NET Framework 1.x 已經有了相當程度的瞭解, 那麼絕對可以從本章中獲益匪淺 本章將會介紹如何使用 ADO.NET 2.0 的物件模型, 建構出有彈性又迅速, 而且還可以視規模進行調整的資料存取物件及應用程式 仔細地說, 我們會把焦點放在 : ADO.NET 2.0 的架構 ADO.NET 2.0 所提供的新功能, 尤其是批次更新,DataSet 的效能提升以及非同步處理 使用共通提供器模型 (Common Provider Model) 建構資料存取元件 11.1 ADO.NET 2.0 在架構上的改進 ADO.NET 2..0 的主要設計目標如下 : 增加許多由客戶主導的新功能, 但仍維持與 ADO.NET 1.x 的相容性改善 ADO.NET1.x 的效能為進階使用者提供更強大的功能利用 SQL Server 2005 的新功能 11-2
ASP.NET 2.0 資料存取 在分散式應用程式中, 使用離線資料 (disconnected data) 已經漸漸成為一種很常見的概念 離線模型表示的是一旦取得所需資料之後, 便關閉和資料來源之間的連線 也就是只在本地端進行資料處理 上述的模型之所以如此風行, 就在於可以釋放寶貴的的資料庫伺服器資源, 因此非常適合用來設計需要高度可調整性的應用程式 ADO.NET 採用的離線資料解決方案就是 DataSet 物件 ADO.NET 的元件 為了更進一步地支援離線模型, ADO.NET 的元件會將資料存取功能與資料處理分開 上述的功能是由 DataSet 與.NET Data Provider 這兩個主要的元件來完成的 圖 11-1 說明了將資料存取與資料處理分開的概念 圖 11-1 DataSet 是 ADO.NET 離線架構的核心元件 DataSet 的設計很明顯是為了與資料來源保持獨立, 因此它可以適用於多種不同的資料來源,XML 資料, 或甚至是用來管理專屬於應用程式的局部性資料, 例如記憶體內的資料快取區 DataSet 中包含了一組構成資料行列的 DataTable 物件, 以及與 DataTable 物件內的資料有關的主要索引, 外部索引, 以及限制和關聯資訊 基本上, 它就是一套記憶體內的資料庫 但最棒的是, 它並不在乎資料是來自於資料庫,XML 檔案, 兩者的組合或甚至是其他來源 您可以在無視資料來源位置的情況下, 將加入 更新以及刪除等動作套用到 DataSet 上, 然後再將更動後的結果送回資料來源 我們將會在本章中更深入地檢視 DataSet 物件系列, 以及 ADO.NET 2.0 針對它所進行的改良 11-3
第十一章 ADO.NET 架構的另一個核心元素是.NET Data Provider, 它的元件都是專為處理 資料而設計的 ( 相對於 DataSet 的資料存取功能 ) 下表中列出了這些元件 物件 Connection Command DataReader DataAdapter 功能提供與資料來源間的連線用來存取各種資料庫命令, 例如傳回及修改資料, 執行預存程序, 以及傳送或取得參數資訊等 提供來自資料來源的高效能唯讀資料流作為 DataSet 物件與資料來源之間的橋樑 DataAdapter 會利用 Command 物件對資料來源執行 SQL 指令, 除了將資料載入 DataSet 之外, 還會將在資料來源中更新對 DataSet 的資料所進行的更動 等我們稍後更仔細地探討 DataAdapter 物件時, 會更進一步地涵蓋上述的主題.NET Framework 2.0 提供了三套.NET Data Provider: SQL Server.NET Data Provider,Oracle.NET Data Provider 以及 OLE DB.NET Data Provider 請不要把 OLE DB.NET Data Provider 與一般的 OLE DB 資料提供器混為一談 在決定應該使用何種資料提供器時, 最簡單的規則就是先使用特定的.NET Relational DatabaseManagement System (RDBMS) 資料提供器 只要有提供的話 如果必須連線到其他未提供的資料來源, 再使用.NET OLE DB Provider 因此, 如果您所設計的應用程式使用的是 SQL Server, 就應該使用 SQL Server.NET Data Provider OLE DB.NET Provider 可用來存取透過 OLE DB 提供資料的資料來源, 例如 Microsoft Access 與 Open DataBase Connectivity(ODBC) 等等 稍後, 我們會有更詳細的說明 11.2.NET Data Providers.NET Data Provider 的用途包括了連線至特定 RDBMS 資料庫 ( 例如 SQL Server 或 Oracle), 執行指令, 以及擷取結果 這些執行結果可能會被直接加以處理 ( 透過 DataReader), 放置在 ADO.NET DataSet 物件中 ( 透過 DataAdapter) 提供給使用者, 抑或結合來自多個來源的資料, 或者在各層之間傳遞.NET Data Provider 的設計目標是降低負擔, 在資料來源與.NET 程式設計者的程式碼間建立最少的層 11-4
ASP.NET 2.0 資料存取 次, 並且在不犧牲任何功能的情況下增加效能 目前,.NET Framework 支援了三種資料提供器 :SQL Server.NET Data Provider( 適用於 Microsoft SQL Server 7.0 以上 ),Oracle.NET Data Provider 以及 OLE DB.NET Data Provider 大部分的 RDBMS 廠商都會建立自己的.NET Data Provider, 以鼓勵.NET 開發者使用他們的資料庫 Connection 物件 要連線至特定的資料來源, 必須使用 Connection 物件 舉例來說, 要連線到 Microsoft SQL Server 7.0 以上的版本, 就必須使用 SQL Server.NET Data Provider 的 SqlConnection 物件 要與 OLE DB 資料來源連線, 可以使用 OLE DB.NET Data Provider 的 OleDbConnection 物件 至於 OLE DB Provider for SQL Server (SQLOLEDB) 則可以連線到 7.0 版以前的 Microsoft SQL Server 連線字串的格式 OleDbConnection 如果是 OLE DB.NET Data Provider, 連線字串的格式與在 ADO 中使用的格式一 樣 不過, 還是有以下這幾點例外 : 必須要有 Provider 關鍵字 不支援 URL, 以及 Remote Provider 及 Remote Server 等關鍵字 以下是連線到 Oracle 資料庫的 OleDbConnection 連線字串範例 ( 請注意總共只有一 行 ): Provider=msdaora;Data Source=MyOracleDB;User Id=myUsername;Password=myPassword; 連線字串的格式 SqlConnection SQL Server.NET Data Provider 支援的連線字串格式和 OLE DB(ADO) 連線字串格式很類似 唯一需要注意的是成對的資料提供器名稱 資料值寫法, 因為我們使用的是 SQL Server.NET Provider 以下是 SqlConnection 連線字串的範例 : data source=(local);initial catalog=pubs; Integrated Security=SSPI; 11-5
第十一章 Command 物件 建立連線之後, 我們可以利用 Command 物件來執行命令, 並且從資料來源 ( 例如 SQL Server) 傳回結果 建立 Command 物件的方法有兩種 : 使用 Command 建構函式, 或是叫用 Connection 物件的 CreateCommand 方法 如果採用前者, 必須標註要在資料來源執行的 SQL 陳述以及 Connection 物件 我們可以透過 CommandText 屬性來查詢及修改 Command 物件的 SQL 陳述 以下的程式就是執行 SELECT 指令並傳回 DataReader 物件的範例 : Build the SQL and Connection strings. Dim sql As String = SELECT * FROM authors Dim connectionstring As String = Initial Catalog=pubs; _ & Data Source=(local);Integrated Security=SSPI; Initialize the SqlCommand with the SQL and Connection strings. Dim command As SqlCommand = New SqlCommand(sql, _ New SqlConnection(connectionString)) Open the connection. command.connection.open() Execute the query, return a SqlDataReader object. CommandBehavior.CloseConnection flags the DataReader to automatically close the DB connection when it is closed. Dim datareader As SqlDataReader = _ command.executereader(commandbehavior.closeconnection) Command 物件的 CommandText 屬性可以執行所有 SQL 陳述, 而非只侷限於標準的 SELECT,UPDATE,INSERT, 以及 DELETE 陳述 舉例來說, 我們可以利用 Command 物件執行適當的 SQL 陳述, 以建立資料表, 外部索引, 主要索引 等等 Command 物件提供了好幾種可以用來進行所需動作的 Execute 方法 利用資料流的形式傳回結果時, 可以使用 ExecuteReader 來傳回 DataReader 物件 ExecuteScalar 則是用來傳回單一資料值 在 ADO.NET 2.0 中, 新加入的 ExecuteRow 方法可以利用 SqlRecord 物件的形式傳回一列資料 ExecuteNonQuery 則是用來執行不會傳回資料列的指令, 其中也包括了具有輸出參數及傳回值的預存程序 (stored procedures) ( 我們會在稍後的篇幅裡介紹預存程序 ) 11-6
ASP.NET 2.0 資料存取 使用 DataAdapter 及 DataSet 的時候, Command 物件的用法則是透過 DataAdapter 物件的 SelectCommand,InsertCommand,UpdateCommand 以及 DeleteCommand 屬性來傳回及修改資料來源處的資料 請留意, 在叫用 DataAdapter 的 Fill 方法前, 必須先設定 SelectCommand 屬性 在叫用 Update 方法前, 必須先設定 InsertCommand, UpdateCommand 以及 DeleteCommand 等屬性 我們會在介紹 DataAdapter 物件時更進一步加以說明 透過 Command 物件使用預存程序 在這一節中, 我們將會簡單地介紹如何使用預存程序 稍後, 我們會在本章中深入地介紹如何建立一個可重複使用的資料存取元件, 在該元件中也會用到預存程序 使用預存程序的動機相當簡單 假設我們有一段指令如下 : SELECT au_lname FROM authors WHERE au_id= 172-32-1176 如果您利用 SqlCommand 的 ExecuteReader 方法將上面的指令傳給 SQL Server( 或任何同目的的執行方法 ), SQL Server 就必須在執行之前先編譯程式碼, 這個情況和 VB.NET 應用程式必須在執行前先進行編譯的作法很類似 上述編譯動作將會佔去 SQL Server 的時間 很明顯地, 如果我們能夠減少 SQL Server 必須進行的編譯工作, 就可以提高資料庫的效能 ( 請比較編譯後的應用程式與直譯式程式碼兩者間的效能差異 ) 這正是預存程序的重要意義 : 我們可以先建立一段程序, 並將之儲存在資料庫中 因為這段程序是已知的, 因此可以在應用程式使用它之前先行編譯就緒 預存程序的使用很簡單, 不過存取這些程序的程式卻相當繁瑣 ( 純屬作者個人觀點 ) 在下一節中, 我們將會探討如何讓存取預存程序的過程變得更簡單 為了說明的方便, 我們會先講解一個簡單的應用程式, 以示範如何建立及呼叫預存程序 11-7
第十一章 建立預存程序 我們可以利用 Visual Studio.NET 或 SQL Server Enterprise Manager 中的工具中來建立預存程序 ( 單純從技術上看來, 您也可以採用其他廠商的工具, 或乾脆使用早期的 SQL 指令碼 ) 在我們的例子中, 會先建立一個依據給定的作者 ID 將所有相關欄位傳回的預存程序 進行這項動作的 SQL 陳述應該如下所示 : SELECT au_id, au_lname, au_fname, phone, address, city, state, zip, contract FROM authors WHERE au_id = whatever author ID we want whatever author ID we want 這個部分相當重要 一般來說, 在使用預存程序時可以提供參數給程序以利後續使用 由於這並不是一本介紹 SQL Server 的書籍, 因此我們只會對這個主題進行原則性的介紹 在 Web 上有相當多關於建立預存程序的資源 ( 這些資料通常都已經存在好一陣子, 而且並不是.NET 的專屬功能 ) SQL Server 中的變數必須在最前面加上 @ 符號 因此, 如果我們所使用的變數名稱是 au_id, 對應的 SQL 程式碼便應該會是 : SELECT au_id, au_lname, au_fname, phone, address, city, state, zip, contract FROM authors WHERE au_id = @au_id 在 Visual Studio 2005 中, 可以利用伺服器總管來存取預存程序 請加入新的資料連線 ( 或使用既有的資料連線 ), 然後進入樹狀管理圖示中的預存程序資料夾 在畫面上, 您可以看見一些已被載入的預存程序 byroyalty 程序是一段由 pubs 資料庫開發者所提供的預存程序 圖 11-2 顯示了 Visual Studio 2005 的 pubs 資料庫預存程序 11-8
ASP.NET 2.0 資料存取 圖 11-2 要建立新的預存程序, 請以滑鼠右鍵點選 預存程序 資料夾, 並選取 新增預存程序, 接著便會顯示編輯器視窗 預存程序可以只是一句 SQL 陳述, 或者是一組很複雜的陳述 T-SQL 還支援了分歧, 迴圈以及其他變數宣告等功能, 可建構出相當複雜的預存程序程式 在此, 我們的預存程序就只是一行 SQL 陳述 我們必須宣告想要傳入的參數 (@au_id) 以及程序的名稱 :usp_authors_get_by_id 以下便是預存程序的程式碼: CREATE PROCEDURE usp_authors_get_by_id @au_id varchar(11) AS SELECT au_id, au_lname, au_fname, phone, address, city, state, zip, contract FROM authors WHERE au_id = @au_id 按下 確定 按鈕將這段程序儲存到資料庫中 接著, 我們就可以利用程式碼來存取這段程序 11-9
第十一章 叫用預存程序 叫用預存程序, 其實就是在建立連線至資料庫的 SqlConnection 物件及執行預存程 序的 SqlCommand 物件 在本章的範例程式中, 我們將會介紹一個名為 Examples.sln 的方案, 其中包含了一 個名為 AdoNetFeaturesTest 的專案 本章的所有資料存取範例程式都必須用到 pubs 資料庫 您可以從 MSDN 中下載 此外, 在執行程式碼範例前, 請在 SQL Server 2005 Management Studio 中執行本章程式碼中的 examples.sql 檔案 這項動作將會在 pubs 資料庫中建立必要的預存程序與功能 現在, 我們必須決定要透過叫用預存程序來傳回哪些資料 在這個例子中, 我們會傳回 SqlDataReader 物件的執行個體 在 TestForm.vb 檔案中, 有一個名為 GetAuthorSqlReader 的方法 這個方法會接受一個作者 ID, 然後傳回 SqlDataReader 物件的執行個體 以下便是這個方法的程式碼 : Private Function GetAuthorSqlReader(ByVal authorid As String) As SqlDataReader Build a SqlCommand Dim command As SqlCommand = New SqlCommand( usp_authors_get_by_id, _ GetPubsConnection()) Tell the command we are calling a stored procedure command.commandtype = CommandType.StoredProcedure Add the @au_id parameter information to the command command.parameters.add(new SqlParameter( @au_id, authorid)) The reader requires an open connection command.connection.open() Execute the sql and return the reader Return command.executereader(commandbehavior.closeconnection) End Function 請留意在 SqlCommand 的建構函式中, 我們把建立與 pubs 資料庫之間的連線這項工作交給另一個輔助函式 稍後, 我們還會在其他程式範例中用到這個函式 以下便是 GetPubsConnection 這個輔助函式的程式碼 : Private Function GetPubsConnection() As SqlConnection Build a SqlConnection based on the config value. Return New _ SqlConnection(ConfigurationSettings.AppSettings( dbconnectionstring )) End Function 在這段程式中, 最重要的工作就是從 app.config 這個應用程式組態檔中取得資料庫的連線字串 以下便是位於 app.config 檔案中的項目 : 11-10
ASP.NET 2.0 資料存取 <appsettings> <add key= dbconnectionstring value= data source=(local);initial catalog=pubs;integrated Security=SSPI; /> </appsettings> 儘管輔助函式進行的功能並不多, 不過將這段程式獨立成一個方法的確是個好主意 現在, 就算需要改變所有與資料庫連線的程式碼, 也只需要更動位於同一處的程式碼 存取預存程序會比利用先前介紹過的方法來存取一般的 SQL 陳述更為複雜 ( 不過並不會比較困難 ) 以下是進行存取的方式: 建立 SqlCommand 物件 設定 CommandType 屬性來調整 SqlCommand 的組態, 以存取預存程序 加入與預存程序相符的參數 利用 SqlCommand 的 Execute 系列方法來執行預存程序 由於這個應用程式並不需要很炫的 UI, 因此我們要接著進行更有趣的討論 我們加入了一個名為 getauthorbyidbutton 的按鈕, 以叫用 GetAuthorSqlRecord 輔助方法, 並且顯示被選取的作者名稱 以下是按鈕的 Click 事件處理程序 : Private Sub _getauthorbyidbutton_click(byval sender As System.Object, _ ByVal e As System.EventArgs) Handles _getauthorbyidbutton.click Dim reader As SqlDataReader = Me. GetAuthorSqlReader ( 409-56-7008 ) If reader.read() MsgBox(reader( au_fname ).ToString() & _ & reader( au_lname ).ToString()) End If reader.close() End Sub 在此, 我們直接在程式中寫入 409-56-7008 這個作者 ID 請執行程式, 即可看到如圖 11-3 的執行結果 圖 11-3 11-11
第十一章 DataReader 物件 我們可以利用 DataReader 物件從資料庫中取出唯讀且只能前進的資料流 使用 DataReader 可以提高應用程式的效能並降低系統負擔, 因為在記憶體中同時只會存在一列資料 利用 DataReader 物件, 我們可以在 ADO.NET 中達到最接近原始資料的程度, 但不必花費產生 DataSet 物件的額外負擔 如果 DataSet 中包含了許多資料, 使用 DataSet 物件的成本將會相當高 至於使用 DataReader 物件的缺點, 則是必須要有開啟的資料庫連線, 因此會增加網路的存取動作 建立 Command 物件的執行個體之後, 只要叫用它的 Command.ExecuteReader 方法, 就可以建立 DataReader 底下就是建立 DataReader 及將它的資料值顯示在畫面上的範例程式 : Private Sub TraverseDataReader() Build the SQL and Connection strings. Dim sql As String = SELECT * FROM authors Dim connectionstring As String = Initial Catalog=pubs; _ & Data Source=(local);Integrated Security=SSPI; Initialize the SqlCommand with the SQL query and connection strings. Dim command As SqlCommand = New SqlCommand(sql, _ New SqlConnection(connectionString)) Open the connection. command.connection.open() Execute the query, return a SqlDataReader object. CommandBehavior.CloseConnection flags the DataReader to automatically close the DB connection when it is closed. Dim reader As SqlDataReader = _ command.executereader(commandbehavior.closeconnection) Loop through the records and print the values. Do While reader.read Console.WriteLine(reader.GetString(1) & & reader.getstring(2)) Loop Close the DataReader (and its connection). reader.close() End Sub 在這段程式中, 我們使用了 SqlCommand 物件, 並透過它的 ExecuteReader 方法來 執行查詢 這個方法會傳回產生的 SqlDataReader 物件 然後, 我們即可利用迴圈 讀取該物件, 並且印出作者名稱 這段程式與利用迴圈讀取 DataTable 資料列的作 11-12
ASP.NET 2.0 資料存取 法最主要的不同在於 : 我們必須在利用迴圈讀取 DataReader 物件內的資料時保持 連線 ; 這是因為 DataReader 一次只會讀取少量的資料流, 以節省記憶體空間 這裡有一個很明顯的爭議點 到底應該使用 DataReader 還是 DataSet? 這個問題的答案其實取決於效能 如果您希望擁有較高的效能, 並且只需要存取接收的資料一次, 那麼 DataReader 的確是正確的選擇 如果您需要存取同樣的資料好幾次, 或者需要在記憶體中模擬相當複雜的關聯 那麼就應該選擇 DataSet 無論如何, 您都應該在決定採用哪一種作法之前先對這兩種選項進行完整的測試 DataReader 物件的 Read 方法可以從查詢結果中取出一列 只要將欄位的名稱或參 考次序 (ordinal reference) 傳給 DataReader, 就可以存取傳回資料列中的欄位 如果 需要最佳的效能,DataReader 還提供了一系列的方法, 可依據欄位原本的資料型 別來存取欄位 (GetDateTime,GetDouble,GetGuid 以及 GetInt32 等等 ) 擷取欄 位時, 如果能夠在知道內部資料型別的情況下使用具有明確型態的存取方法, 可以減少必要的型別轉換時間 ( 因為必須從 Object 型別轉換過來 ) DataReader 提供了無緩衝區的資料流, 可以利用程序式邏輯有效率地循序處理來自資料來源的結果 在擷取大量資料時,DataReader 是一種很好的選擇 ; 一次只有一列資料會被保留在記憶體快取中 當 DataReader 物件使用完畢後, 一定要叫用 Close 方法, 並且關閉 DataReader 物件的資料庫連線 否則, 連線將不會被關閉, 直到到記憶體回收行程回收物件時為止 請留意我們如何在 SqlDataReader.ExecuteReader 方法中使用 CommandBehavior.CloseConnection 列舉值 這種作法可以要求 SqlCommand 物件在叫用 SqlDataReader.Close 方法時自動關閉資料庫連線 在您的 Command 中所包含的輸出參數或傳回值, 必須等 DataReader 關閉之後才能使用 非同步地執行命令 在 ADO.NET 2.0 中, 可以透過額外的支援讓 Command 物件非同步地執行指令 在許多應用程式中, 這項功能都可以讓效能獲得大幅度的提升, 尤其是 Windows Forms 應用程式 這項功能在實際應用上可能會相當實用, 尤其是在您必須執行長 11-13
第十一章 時間進行的 SQL 陳述時 我們將會介紹如何利用這項新功能來加強應用程式回應 使用者的效果 SqlCommand 物件提供了不同的非同步呼叫選項, 包括 BeginExecuteReader, BeginExecuteNonQuery 以及 BeginExecuteXmlReader 每個方法都有對應的 End 方法, 也就是 EndExecuteReader, EndExecuteNonQuery 以及 EndExecuteXmlReader 由於我們已經介紹了 DataReader 物件, 因此將會在接下來 的範例中利用 BeginExecuteReader 方法來執行長時間的查詢動作 在 AdoNetFeaturesTest 專案中, 我們在表單上加入了按鈕與相關的 Click 事件處理 程式, 以啟動取得 DataReader 執行個體的非同步呼叫動作 : Private Sub _testasynccallbutton_click(byval sender As System.Object, _ ByVal e As System.EventArgs) Handles _testasynccallbutton.click Build a connection for the async call to the database. Dim connection As SqlConnection = GetPubsConnection() connection.connectionstring &= Asynchronous Processing=true; Build a command to call the stored procedure. Dim command As New SqlCommand( usp_long_running_procedure, connection) Set the command type to stored procedure. command.commandtype = CommandType.StoredProcedure The reader requires an open connection. connection.open() Make the asynchronous call to the database. command.beginexecutereader(addressof Me.AsyncCallback, _ command, CommandBehavior.CloseConnection) End Sub 第一項進行的動作是重複使用 GetPubsConnection 這個輔助程式, 以取得和 Pubs 資 料庫之間的連線 接著, 我們將 Asynchronous Processing = true 這句陳述附加到 Connection 物件的連線字串後面 這項動作相當重要, 因為它是利用 ADO.NET 2.0 對 SQL Server 進行非同步呼叫時的必要設定動作 設定好連線之後, 我們會建立 SqlCommand 物件, 並且透過初始化將之設定為執行 usp_long_running_procedure 預存程序 這段預存程序會在執行 usp_authors_get_all 預存程序前使用 SQL Server 2005 的 WAITFOR DELAY 陳述 產生 20 秒鐘的延遲時間 如同您所猜測的,usp_Authors_Get_All 這個預存程序會 11-14
ASP.NET 2.0 資料存取 從作者資料表中選取所有的作者 上述的延遲時間只是為了示範在執行上述預存程序的同時, 我們還是可以在 Windows Forms 應用程式中執行其它的工作 以下是 usp_long_running_procedure 預存程序 : CREATE PROCEDURE usp_long_running_procedure AS SET NOCOUNT ON WAITFOR DELAY 00:00:20 EXEC usp_authors_get_all 在按鈕的 Click 事件處理程序中, 我們在最後一行呼叫了 BeginExecuteReader 在這個呼叫中, 我們先傳入了一個 System.AsyncCallback 委派型別的委派方法 (Me.AsyncCallBack) 這是.NET Framework 在該方法以非同步的方式執行完畢後回呼我們的方式 接著, 我們將已初始化的 SqlCommand 物件傳入, 讓它開始執行 另外, 還傳入了 DataReader 所需的 CommandBehavior 值 在這個範例程式中, 我們傳入了 CommandBehavior.CloseConnection 值, 以利在關閉 DataReader 之後一併關閉資料庫連線 我們會在下一節中更仔細地討論 DataReader 現在我們已經啟動了非同步呼叫, 並且為非同步呼叫定義了回呼函式 現在, 我們要檢視這個實際上進行回呼動作的 AsyncCallback 方法 : Private Sub AsyncCallback(ByVal ar As IAsyncResult) Get the command that was passed from the AsyncState of the IAsyncResult. Dim command As SqlCommand = CType(ar.AsyncState, SqlCommand) Get the reader from the IAsyncResult. Dim reader As SqlDataReader = command.endexecutereader(ar) Get a table from the reader. Dim table As DataTable = Me.GetTableFromReader(reader, Authors ) Call the BindGrid method on the Windows main thread, passing in the table. Me.Invoke(New BindGridDelegate(AddressOf Me.BindGrid), _ New Object() {table}) End Sub 在這段程式的第一行中進行的動作, 是從傳入的 IAsyncResult 的 AsyncState 屬性中取得 SqlCommand 物件 請記住, 我們在先前呼叫 BeginExecuteReader 時已經傳入了 SqlCommand 物件 我們必須先取得它, 才能呼叫下一行的 EndExecuteReader 方法 這個方法會傳回 SqlDataReader 在下一行中, 我們會將 SqlDataReader 轉換為 DataTable( 稍後我們會在討論 DataSet 時涵蓋上述的轉換動作 ) 在這個方法中, 最重要的可能是最後一行 如果我們試著將 DataTable 繫結 11-15
第十一章 到資料表格上, 這項動作將會失敗, 因為目前我們所執行的並不是主要的 Windows 執行緒 我們有一個負責資料繫結動作的 BindGrid 輔助函式, 但只能在 Windows 主執行緒中叫用它 為了取得主執行緒中的資料, 我們必須利用 Form 物件的 Invoke 方法來封送 (marshal) 它 Invoke 一共有兩個引數 : 您希望呼叫的方法委 派, 以及 ( 選擇性的 ) 該方法的參數 在這個範例中, 我們定義了 BindGrid 方法 的委派, 稱之為 BindGridDelegate 以下是委派的宣告 : Private Delegate Sub BindGridDelegate(ByVal table As DataTable) 請留意這個函式的簽章和底下的 BindGrid 方法完全相同 : Private Sub BindGrid(ByVal table As DataTable) Clear the grid. Me._authorsGridView.DataSource = Nothing Bind the grid to the DataTable. Me._authorsGridView.DataSource = table End Sub 以下是另一種叫用表單 Invoke 方法的方式 : Me.Invoke(New BindGridDelegate(AddressOf Me.BindGrid), _ New Object() {table}) 在上面的程式碼中, 我們傳入了新的 BindGridDelegate 執行個體, 並且利用指向 BindGrid 方法的指標進行初始化 因此, 執行查詢的.NET 工作執行緒將可以安全地加入主要的 Windows 執行緒 DataAdapter 物件.NET Framework 中的每個.NET Data Provider 都有自己的 DataAdapter 物件 :OLE DB.NET Data Provider 提供的是 OleDbDataAdapter 物件,SQL Server.NET Data Provider 則有 SqlDataAdapter 物件 DataAdapter 物件可以從資料來源取得資料, 並且產生 DataTables 及在 DataSet 中的限制條件 另外,DataAdapter 也會解析對 DataSet 進行的更動, 並將之送回給資料來源 DataAdapter 在與資料來源連線時使用的是.NET Data Provider 的 Connection 物件 這一點和 DataReader 並不相同, 因為 DataReader 會使用 Connection 物件直接存取資料, 而不必用到 DataAdapter DataAdapter 基本上是將 DataSet 物件與實際的資料來源分開, 而 DataReader 則是以唯讀的形式與資料緊密地綁在一起 11-16
ASP.NET 2.0 資料存取 DataAdapter 的 SelectCommand 屬性是一個可從資料來源取得資料的 Command 物 件 您可以透過將 Command 物件傳入 DataAdapter 建構函式, 很方便地設定 DataAdapter 的 SelectCommand 屬性 至於 DataAdapter 的 InsertCommand, UpdateCommand 以及 DeleteCommand 屬性, 則是根據 DataSet 中的資料修改動作 來管理資料來源的資料更新 DataAdapter 的 Fill 方法可以根據執行 DataAdapter 的 SelectCommand 的結果來產生 DataSet 此外, 還會加入或更新 DataSet 中的資 料列以符合資料來源中的資料 在下面的範例中, 我們會介紹如何將 pubs 資料庫 內 authors 資料表的資訊填入 DataSet 物件 : Private Sub TraverseDataSet() Build the SQL and Connection strings. Dim sql As String = SELECT * FROM authors Dim connectionstring As String = Initial Catalog=pubs; _ & Data Source=(local);Integrated Security=SSPI; Initialize the SqlDataAdapter with the SQL and Connection strings, and then use the SqlDataAdapter to fill the DataSet with data. Dim adapter As New SqlDataAdapter(sql, connectionstring) Dim authors As New DataSet adapter.fill(authors) Iterate through the DataSet s table. For Each row As DataRow In authors.tables(0).rows Console.WriteLine(row( au_fname ).ToString _ & & row( au_lname ).ToString) Next Print the DataSet s XML. Console.WriteLine(authors.GetXml()) Console.ReadLine() End Sub 請留意我們如何利用 SqlDataAdapter 的建構函式來傳入及設定 SelectCommand, 並且以傳入連線字串的方式來代替已經將 Connection 屬性初始化完畢的 SqlCommand 物件 然後, 我們只需要叫用 SqlDataAdapter 物件的 Fill 方法, 並傳 入已經初始化的 DataSet 物件即可 如果 DataSet 物件並未初始化,Fill 方法就會 產生例外 (System.ArgumentNullException) 在 ADO.NET 2.0 中, 利用 DataAdapter 更新資料庫的效能已經有了大幅度的改 進 在 ADO.NET 1.x 中,DataAdapter 的 Update 方法會透過迴圈來檢閱 DataSet 中每個 DataTable 物件的每一列, 接著根據要更新的每一列資料依序進行與資料庫 之間的往返更新動作 在 ADO.NET 2.0 中,DataAdapter 加入了對批次更新動作的 11-17
第十一章 支援 在每次叫用 Update 方法的時候,DataAdapter 只需要與資料庫進行一次資料 往返, 就可以完成 DataSet 中的所有更新動作 現在讓我們看看底下這個利用 DataAdapter 物件來插入, 更新以及刪除 DataTable 中的資料, 並更新 Pubs 資料庫的進階範例 : Private Sub _batchupdatebutton_click(byval sender As System.Object, _ ByVal e As System.EventArgs) Handles _batchupdatebutton.click Build insert, update, and delete commands. Build the parameter values. Dim insertupdateparams() As String = { @au_id, @au_lname, @au_fname, _ @phone, @address, @city, @state, @zip, @contract } 這段程式在一開始先進行字串陣列的初始化, 陣列中存放的是要傳給 BuildSqlCommand 輔助函式的參數名稱 Insert command. Dim insertcommand As SqlCommand = BuildSqlCommand( usp_authors_insert, _ insertupdateparams) 接著, 我們將欲執行的預存程序名稱以及該程序的參數傳給 BuildSqlCommand 輔 助方法 這個方法會傳回已初始化完畢的 SqlCommand 執行個體 以下便是 BuildSqlCommand 輔助函式 : Private Function BuildSqlCommand(ByVal storedprocedurename As String, _ ByVal parameternames() As String) As SqlCommand Build a SqlCommand. Dim command As New SqlCommand(storedProcedureName, GetPubsConnection()) Set the command type to stored procedure. command.commandtype = CommandType.StoredProcedure Build the parameters for the command. See if any parameter names were passed in. If Not parameternames Is Nothing Then Iterate through the parameters. Dim parameter As SqlParameter = Nothing For Each parametername As String In parameternames Create a new SqlParameter. parameter = New SqlParameter() parameter.parametername = parametername Map the parameter to a column name in the DataTable/DataSet. parameter.sourcecolumn = parametername.substring(1) Add the parameter to the command. command.parameters.add(parameter) Next End If Return command End Function 11-18