使用 ADO.NET 的最佳經驗分享
作者:Dennis Lu
作者:Doug Rothaus
Microsoft Corporation
張貼日期:2003 年 7 月
適用於:
有使用 Microsoft® ADO.NET 及 Microsoft® .NET Framework 經驗的開發人員
摘要:Microsoft ADO.NET 程式碼的最佳經驗分享,以及使用 ADO.NET 所提供物件進行開發的相關建議。(列印共 21 頁)
附註 如果您並不熟悉 ADO.NET 和 .NET Framework,請參考 .NET Framework SDK 中的 Accessing Data with ADO.NET。如果您是 ADO 開發人員,有興趣將您的應用程式遷移到 ADO.NET,請參考 ADO.NET for the ADO Programmer 取得更進一步的資訊。
內容
簡介
.NET Framework 資料供應者
使用 DataReaders、DataSets、DataAdapters 以及 DataViews
使用命令
使用資料連接
和 XML 的整合
更多有用的秘訣
簡介
本篇文章提供您如何在 ADO.NET 應用程式中實作及達成最佳執行效能、延展性和功能性的最佳解決方案;它同時也涵蓋了在使用 ADO.NET 中的物件上的經驗,以及相關的建議,協助您對您的 ADO.NET 應用程式進行最佳化的設計。
這篇文章包含了以下的主題:
- 有關 .NET Framework 所提供的 .NET Framework 資料提供者的資訊。
- 比較 DataSet 及 DataReader,同時解釋這些物件的最佳使用時機。
- 說明如何使用 DataSet、Commands 以及 Connections。
- 整合 XML 的資訊。
- 一般的秘訣和議題。
有關 ADO.NET 最佳經驗心得的相關資訊,請參考 MSDN Library 中的 NET Data Access Architecture Guide。請注意,.NET Data Access Architecture Guide 主要著重在使用 Microsoft® SQL Server™ 7.0 或後續版本上的架構。
以下的資源提供了有關 ADO.NET 的其它相關資訊:
- 新聞群組:BDA 新聞群組可以利用 NNTP 新聞閱讀器在以下的網址取用: news://msnews.microsoft.com/microsoft.public.dotnet.framework.adonet 或是透過您的 Web 瀏覽器在以下網址取用: http://msdn.microsoft.com/newsgroups/loadframes.asp?icp=msdn&slcid=us&newsgroup=microsoft.public.dotnet.framework.adonet。
- 討論群組:
.NET Framework 資料提供者
在 .NET Framework 中的資料提供者是一個應用程式和資料來源之間的橋樑。一個 .NET Framework 資料提供者可以讓您傳回來自資料來源的查詢結果,對資料來源執行命令,並將 DataSet 中的變動傳遞回資料來源。本文包含有選擇最合用 .NET Framework 資料提供者的秘訣。
要使用那一個 .NET Framework 資料提供者?
您的應用程式要達到最佳的執行效能,就必須選擇最適合您的資料來源的 .NET Framework 資料提供者。您的應用程式可以有不少資料提供者的選項。下表中列出了可用資料提供者以及資料提供者最適合的資料來源。
提供者 | 詳細資料 |
---|---|
SQL Server .NET 資料提供者 | 位在 System.Data.SqlClient 命名空間中。
適合使用 Microsoft SQL Server 7.0 及後續版本的中介層應用程式中。 適合使用 Microsoft Data Engine (MSDE) 或 Microsoft SQL Server 7.0 及後續版本的單層應用程式。 對於 Microsoft SQL Server 6.5 版及更早的版本,您必須透過 OLE DB .NET 資料提供者使用 OLE DB Provider for SQL Server 。 |
OLE DB .NET 資料提供者 | 位在 System.Data.OleDb 命名空間中。
適合使用 Microsoft SQL Server 6.5 或更早版本的中介層應用程式,或者是任何列在 .NET Framework SDK 的 OLE DB Interfaces Used by the OLE DB .NET Data Provider 一文中支援 OLE DB 介面的 OLE DB 供應者。(並不需要 OLE DB 2.5 介面) 對於 Microsoft SQL Server 7.0 及後續版本,.NET Framework 的 SQL Server 資料提供者較為合適。 適合使用 Microsoft® Access 資料庫的單層應用程式。不建議將 Access 資料庫使用在中介層的應用程式。 對於 OLE DB Provider for ODBC (MSDASQL) 的支援會被停用。要存取 ODBC 資料來源,可以下載 ODBC .NET 資料提供者,也會附在.NET Framework SDK 1.1 版中。 |
ODBC .NET 資料提供者 | ODBC .NET 資料提供者可以在網站上下載。
位在 Microsoft.Data.Odbc 命名空間中。 可以存取透過 ODBC 驅動程式連接的資料來源。 注意 ODBC .NET 資料提供者會從 1.1 版開始附在後續的 .NET Framework 中。ODBC .NET 資料提供者所在的命名空間為 System.Data.Odbc。 |
Oracle 的 .NET 資料提供者 | Microsoft .NET Data Provider for Oracle 可在網站上下載。
位在 System.Data.OracleClient 命名空間中。 可存取 Oracle 資料來源 (8.1.7 版及後續版本)。 注意 .NET Data Provider for Oracle 從 1.1 版開始會附在 .NET Framework 中。 |
Custom .NET 資料提供者 | ADO.NET 提供了一組最精簡的介面讓您能實作自己的 .NET Framework 資料提供者。有關如何建立自訂的資料提供者的相關資訊,請參考 .NET Framework SDK 中 Implementing a .NET Data Provider 一文。 |
SQLXML 受管理的類別 | XML for Microsoft SQL Server 2000 (SQLXML 3.0) 當中包含有 SQLXML 受管理的類別讓您能存取 Microsoft SQL Server 2000 及後續版本來自 .NET Framework 的 XML 功能。例如,這些類別讓您能執行 XML 範本,對伺服器上的資料執行 XML 路徑語言 (XPath) 查詢,或是利用 Updategrams 或 Diffgrams 進行資料的更新。
以 SQLXML 1.0 及 2.0 的功能為基礎,SQLXML 3.0 帶入了 Web Service 到 SQL Server 2000 的環境。藉由 SQLXML 3.0,預存程序和 XML 範本可以透過 SOAP 以 Web Service 的型式對外提供。 SQLXML 3.0 可以在網路上 下載。 |
連接到 SQL Server 7.0 或續後版本
在連接到 Microsoft SQL Server 7.0 或後續版本時要得到最佳的執行效能,請使用 SQL Server .NET 資料提供者。SQL Server .NET 資料提供者是設計來直接存取 SQL Server 而不藉助任何額外的技術。圖 1 說明了存取 SQL Server 7.0 或後續版本之間不同技術上的差異。
圖 1. 存取 SQL Server 7.0 或後續版本的連接方法
連接到 ODBC 資料來源
ODBC .NET 資料提供者位在 Microsoft.Data.Odbc 命名空間中,具有和 .NET Data Provider for SQL Server 及 OLE DB 的相同架構。ODBC .NET 資料提供者 (可在網上 下載) 遵循前置有「ODBC」的命名慣例 (例如 OdbcConnection),並且使用標準的 ODBC 連接字串。
注意 ODBC .NET 資料提供者從 1.1 版開始會附在後續的 .NET Framework 中。而包含有 ODBC .NET 資料提供者的命名空間是 System.Data.Odbc。
使用 DataReaders、DataSets、DataAdapters 以及 DataViews
ADO.NET 提供了兩個物件來取用關聯式資料並儲存在記憶體內:DataSet 和 DataReader。DataSet 提供了資料在記憶體內的關聯式表示法,讓資料能以資料表的方式處理、排列,同時儲存對資料的限制,以及資料表之間的關聯。DataReader 則透過快速,順向類型的唯讀資料流來取用資料庫中的資料。
當使用 DataSet 時,您當會利用 DataAdapter (還可能有 CommandBuilder) 來和資料來源互動。同時,在使用 DataSet 時,您也可以藉由 DataView 來套用排序及篩選的功能到 DataSet 的資料上。DataSet 也可以透過繼承的方式產生強型別 (strongly-typed) 的 DataSet 以強型別物件的屬性的方式來取用資料表、資料列和資料欄。
以下的主題提供了何時使用 DataSet 或 DataReader 的資訊,以及如何以最佳化的方式存取其中的資料,和如何以最佳化的方式使用 DataAdapter (包括 CommandBuilder) 及 DataView 的秘訣。
DataSet 和 DataReader
當您在設計應用程式時,要判斷是使用 DataSet 或 DataReader,不妨考量應用程式所需要功能的等級如何來決定。
當您的應用程式要進行以下的作業時,可以考慮使用 DataSet:
- 在多個個別的資料表間巡覽內容。
- 要操作來自不同資料來源的資料 (例如,要處理綜合了多個資料庫、或是 XML 檔以及試算表中的資料)。
- 在各個應用層之間交換資料或是使用到一個 XML Web service。和 DataReader 不同的是,DataSet 可以傳給遠端的前端程式。
- 重複使用同一組資料列構成的組合,以藉由快取的機制加快執行效能 (像是進行排序、搜尋或是資料的篩選)。
- 對個別的資料列進行大量的處理作業。若是對經由 DataReader 所傳回的資料列進行大量的計算處理作業,會綁住服務 DataReader 的資料連接,遠超過真正需要的時間,影響到執行效能。
- 要利用 XML 作業─像是 XSLT 轉換或是 XPath 查詢來處理資料時。
當您的應用程式是進行以下作業時,請使用 DataReader:
- 不需要快取保留資料時。
- 要處理的資料集太大,不適合載入記憶體時。
- 只需要快速的取用資料一次,並且以順向類型和唯讀的方式進行。
注意 DataAdapter 在填入 DataSet 時會使用到 DataReader 。因此,當您使用 DataAdapter 來替代 DataSet 所獲得執行效益主要表現在節省 DataSet 所耗用的記憶體,以及操作 DataSet 所耗費的計算週期上。這樣的執行效益基本上還是要依據您對所需功能的判斷來做出最好的決定。
使用強型別資料集的優點
另一個使用 DataSet 的優點是它可以透過繼承的方式產生強型別的 DataSet。強型別的 DataSet 所帶來的優點包括了能在設計時期進行檢查,並能利用到 Microsoft® Visual Studio® .NET 的自動完成敘述功能。當您的 DataSet 有了固定的結構描述或關聯式結構時,就可以產生強型別的 DataSet,並且以物件的屬性來代表欄位或資料列,而不是透過集合物件項目的方式進行。例如,透過資料列欄位名稱的方式去取用客戶資料表的內容,您可以提供一個 Customer 物件的 Name 屬性。一個有型別的 DataSet 是衍生自 DataSet 類別,所以您不會犧牲掉任何 DataSet 的功能。也就是說,一個具型別的 DataSet 依舊可以進行遠端作業,也可以做為資料繫結控制項─例如 DataGrid - 的資料來源。如果事前不知道結構描述,您還是可以從一般的 DataSet 獲益,但是無法取得強型別的 DataSet 額外的好處。
在強型別的資料集中處理空值
當使用一個強型別的 DataSet 時,您可以利用 XML 結構描述定義語言 (XSD) 註明 DataSet 的結構來確保您的強型別DataSet 能正確的處理空值參照。nullValue 註記可以讓您以特定的值、String.Empty 來取代 DBNull、或是保留空值參照、或是丟出例外。您要選擇那一種作法端視您的應用程式的需求。預設的情況下,當遇到空值參照時會丟出例外。
更進一步的資訊請參考 Working with a Typed DataSet。
在資料集中更新資料
如果您想要以來自伺服器的更新值來更新您 DataSet 中的資料值,可以使用 DataAdapter.Fill。如果您在 DataTable上有定義主索引鍵,DataAdapter.Fill 會依據主索引鍵來對照資料列,並將伺服器上更動的結果套用到既有資料列上。即使在更新之前資料列的值已經被更改過,被更新資料列的 RowState 屬性都還是會指定成 Unchanged。注意到,如果 DataTable 沒有定義主索引鍵,DataAdapter.Fill 會加入新資料列,可能造成重複的主索引鍵。
如果您想要以來自伺服器目前的值來更新資料表,並保留在資料表中既有資料列的變動,您要先進行 DataAdapter.Fill,填入到一個新的DataTable,然後再用 Merge 將該 DataTable 併入 DataSet 中,同時指定 preserveChanges 的值為 true。
在資料集中找尋資料
在 DataSet 中找尋符合特定條件的資料列時,您可以善用以索引為基礎的查詢作業來增加搜尋的執行效能。當您指定 PrimaryKey 給一個 DataTable 物件時,就會產生一個索引。當您為 DataTable 產生一個 DataView 時,也會產生一個索引。下面是善用以索引為基礎的查詢的例子。
- 如果查詢是對構成 DataTable 的 PrimaryKey 的欄位進行,請使用 DataTable.Rows.Find 而不要使用 DataTable.Select。
- 對於牽涉到非主索引鍵的欄位進行的查詢,您可以利用 DataView 來增加多重查詢的執行效能。當您對 DataView 套用排序順序時,在搜尋時會產生一個索引。DataView 提供了 Find 及 FindRows 方法來查詢所依據的 DataTable 中的資料。
- 如果您不需要資料表排序過的檢視方式,您還是可以藉由產生 DataTable 物件的 DataView 來善用以索引為基礎的查詢所帶來的好處,這個好處只在您對資料進行多次查詢時才有用。如果您只進行一次查詢,產生索引的程序反而可能降低因為使用索引所帶來執行效能上的效果。
建構 DataView
當建立 DataView以及 Sort、RowFilter 或 RowStateFilter 屬性改變時,同時會為底層的 DataTable 中的資料建立索引。在建立 DataView 物件時,可以使用 DataView 建構元並以 Sort、RowFilter和RowStateFilter 的值做為建構元的引數 (還有所依據的 DataTable)。結果是索引只會建立一次。若是產生一個「空的」DataView ,再指定 Sort、RowFilter 或 RowStateFilter 屬性的值會導致索引至少建立兩次。
分頁
ADO.NET 提供您在資料從資料來源傳回時額外的控制能力,以及要將多少資料快取在本地的 DataSet中。要對查詢的結果進行分頁的作業方式並不是唯一的,旦是在設計您的應用程式時有一些秘訣可以考慮。
- 避免使用 DataAdapter.Fill 中採用 startRecord 和 maxRecords 的值做為引數的多載型式。當以這種方式填入 DataSet 時,DataSet 只會填入 maxRecords 參數中所指定個數的紀錄(由 startRecord 參數所指定的紀錄開始),但是傳回的還是整個查詢的結果。結果是花費資源處理並不需要的紀錄,同時也耗用不必要的伺服器資源來傳回額外的紀錄。
- 一次只有傳回一頁紀錄的技巧之一是建立一個組合了 WHERE 子句及一個 ORDER BY 子句的 SQL 敘述,同時搭配 TOP 述語。這個技巧要依賴能夠辨識出每一個個別的資料列。當巡覽到下一頁的紀錄時,就修改 WHERE 子句包含所有唯一的辨識代碼大於目前頁面最後一個辨識代碼的紀錄。當巡覽到前一頁的紀錄時,則是修改 WHERE 子句傳回所有唯一辨識代碼小於目前頁面第一筆唯一辨識代碼的紀錄。針對這兩個查詢,都只傳回前幾筆由 TOP 述語所指定的紀錄。當巡覽到前一頁時,您需要將紀錄以遞減的方式排列。這樣就會有效的傳回該查詢的底頁紀錄 (而在顯示之前可能還要再重新對取回的結果進行排序)。有關這個技巧的範例,可以參考 Paging Through a Query Result 一文。
- 另一個一次只傳回一頁紀錄的技巧是建立一個組合了 TOP 述語及內嵌 SELECT 敘述的 SQL 敘述。這個技巧並不需要依賴辨識唯一一筆紀錄的方法。這個技巧的第一筆是將頁面的大小與所要顯示的頁面數相乘,然後將這個數字傳入到 SQL 敘述的 TOP 述語中,以遞增的方式排列。然後將這個查詢嵌在另一個查詢中,只從內嵌的查詢裡挑出 TOP 述語所指定的一頁紀錄數,並以遞減的方式排列。這樣,就會傳回內嵌查詢中底頁的資料頁。例如,要傳回第三頁的查詢結果,每一頁是 10 筆紀錄,您可能會執行以下的命令:
SELECT TOP 10 * FROM (SELECT TOP 30 * FROM Customers ORDER BY Id ASC) AS Table1 ORDER BY Id DESC
注意到這個查詢的結果會是以遞減的方式排列,您可以自行再改變排列的順序。
- 如果您的資料不常改變,您可以藉由一個 DataSet 物件在本地維護紀錄的快取。例如,您可以儲存 10 頁的資料在本地的 DataSet 中,並且只有在使用者巡覽到超出快取中第一頁和最後一頁的資料時,才去查詢資料來源取得新的資料。
相關的資訊請參考 .NET Data Access Architecture Guide。
配合結構描述填入資料集
當在 DataSet 中填入資料時,DataAdapter.Fill 方法會使用 DataSet 既有的結構描述,並以 SelectCommand 傳回的資料來處理。如果在 DataSet 中沒有符合的資料表名稱可以填入,Fill 方法會建立新的資料表。預設的情況下,Fill 只定義欄位和欄位型別類型。
您可以覆寫 Fill 的預設行為,方式是指定 DataAdapter 的 MissingSchemaAction 屬性。例如,要讓 Fill 建立一個資料表結構,同時也包含有主索引鍵、唯一限制、欄位屬性、是否允許空值、欄位最大長度、唯讀欄位以及自動增量欄位等資訊,可以指定 DataAdapter.MissingSchemaAction 的值為 MissingSchemaAction.AddWithKey。此外,您也可以在呼叫 DataAdapter.Fill 前先呼叫 DataAdapter.FillSchema 以確保在填入 DataSet 時已經準備好結構描述。
呼叫 FillSchema 會導致對伺服器額外的來回作業,以取得相關的結構描述資訊。要取得最佳的效能,可以在呼叫 Fill 之前指定 DataSet 的結構描述,或是指定 DataAdapter 的 MissingSchemaAction。
CommandBuilder 的最佳經驗分享
CommandBuilder 會依據 DataAdapter 的 SelectCommand 屬性自動產生 DataAdapter 的 InsertCommand、UpdateCommand 以及 DeleteCommand 屬性,讓 SelectCommand 能進行單一資料表的 SELECT 作業。下面是使用 CommandBuilder 時取得最佳執行效能的一些秘訣。
- CommandBuilder 只能使用在設計時期或是隨機操作的情境。產生 DataAdapter 命令屬性的過程會影響到執行效能。如果您事先知道 INSERT/UPDATE/DELETE 敘述的內容,就明確的自行宣告。一個良好的設計秘訣是為您的 INSERT/UPDATE/DELETE 命令產生預存程序,並明確的指定 DataAdapter 命令屬性來使用它們。
- CommandBuilder 使用了 DataAdapter 的 SelectCommand 屬性來決定其他命令屬性的值。如果 DataAdapter 的 SelectCommand 本身有任何變動的話,務必呼叫 RefreshSchema 來更新命令屬性。
- 當命令屬性為空值時,CommandBuilder 只會為 DataAdapter 的命令屬性產生一個命令 (命令屬性預設是空值)。如果您明確的指定一個命令屬性,CommandBuilder 並不會覆寫其中的內容。如果您希望 CommandBuilder 為已經指定內容的命令屬性產生對應的命令,請指定命令屬性為空值。
批次 SQL 敘述
許多資料庫支援將多個命令組合在單一的命令執行環境中,或是以批次的方式處理。例如,SQL Server 允許您利用分號來隔離多個命令。組合多個命令到單一的命令中可以減少伺服器之間的來回,也可以增加應用程式的效能。例如,您可以將所有必要的刪除動作儲存在應用程式中,然後利用一個批次命令的呼叫從資料來源中將它們刪除。
這雖然能夠增進效能,也可以在管理 DataSet 中的資料更新狀況時增加複雜度。要維持簡易性,您可能會想要為您 DataSet 中的每一個 DataTable 產生一個 DataAdapter。
填入多張資料表到資料集中
如果您使用一個批次的 SQL 敘述來取出多份資料表,並填入一個 DataSet,那麼第一份資料表會以 Fill 方法中所指定的名稱來命名。後續的資料表會以 Fill 方法所指定的名稱再加上一個從一開始的數字陸續增加。例如,若是您執行以下的程式碼:
'Visual Basic Dim da As SqlDataAdapter = New SqlDataAdapter("SELECT * FROM Customers; SELECT * FROM Orders;", myConnection) Dim ds As DataSet = New DataSet() da.Fill(ds, "Customers") //C# SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM Customers; SELECT * FROM Orders;", myConnection); DataSet ds = new DataSet(); da.Fill(ds, "Customers");
從 Customers 資料表傳回的資料是放在叫做「Customers」的 DataTable 中。而從 Orders 資料表中傳回的資料則是安排在一個叫做「Customers1」的 DataTable 中。
您可以在 DataSet 的填入動作完成後很容易的修改「Customers1」資料表的 TableName 屬性值為「Orders」。然而,後續的填入作業會讓「Customers」資料表重新被填入,但是忽略掉「Orders」資料表,並且產生另一個「Customers1」資料表。要避免這個情況,請利用 DataTableMapping 將「Customers1」對照到「Orders」,也將其他的資料表對照到後續的資料表中。例如:
'Visual Basic Dim da As SqlDataAdapter = New SqlDataAdapter("SELECT * FROM Customers; SELECT * FROM Orders;", myConnection) da.TableMappings.Add("Customers1", "Orders") Dim ds As DataSet = New DataSet() da.Fill(ds, "Customers") //C# SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM Customers; SELECT * FROM Orders;", myConnection); da.TableMappings.Add("Customers1", "Orders"); DataSet ds = new DataSet(); da.Fill(ds, "Customers");
使用 DataReader
下面所列是一些使用 DataReader 時取得最佳化執行效能的秘訣,以及在使用 DataReader 時所碰到共同問題的答案:
- DataReader 在取用任何相關 Command 物件的輸出參數之前必須要關閉。
- 當您完成資料讀取作業時務必關閉 DataReader。如果您所使用的 Connection 物件只是用來傳回 DataReader,就在關閉 DataReader 之後立刻關閉它。
另一種明確關閉 Connection 的方式是傳入 CommandBehavior.CloseConnection 給 ExecuteReader 方法,來確保當 DataReader 關閉時會關掉相關的連接。這在您是透過一個方法傳回 DataReader,而沒有控制權去關閉 DataReader 或相關連接時特別有用。
- DataReader 無法在不同的應用層之間進行遠端作業。DataReader 是設計來處理連接的資料存取作業。
- 利用有型別類型的存取元─像是 GetString、GetInt32 等等─來取用欄位資料。這可以省下您對 GetValue 所傳回的 Object 型別類型物件進行必要轉型作業的時間。
- 一個連接一次只能開啟一個 DataReader。在 ADO 中,如果您開啟單一的連接,並透過順向類型唯讀的來要求兩個紀錄集時,ADO 會私下開啟第二個不在集區內的連接,連到資料來源,一直到游標結束生命期為止。在 ADO.NET 中,私下進行的作業十分有限。如果您希望同時對同一個資料來源開啟兩個 DataReaders,您就必須自行開啟兩個資料連接,每一個 DataReader 對應到一個。這讓 ADO.NET 能夠幫助您有更多的控制能力來使用集區中的連接。
- 預設情況下,DataReader 對每一個 Read 動作會載入整個紀錄到記憶體中。所以我們可以隨機存取目前紀錄的欄位內容。如果這種隨機存取的能力不是必要的,為了增加執行效能,請傳入 CommandBehavior.SequentialAccess 給所呼叫的 ExecuteReader。這樣會改變 DataReader 的預設行為,只在被要求時才載入資料到記憶體中。注意到,CommandBehavior.SequentialAccess 要求您按照欄位的順序取用其中的內容。也就是說,一旦您讀取了一個傳回的欄位,就再也不能讀取它的值了。
- 如果您結束了從 DataReader 讀取資料,但是還有很多尚未讀取的結果在等待作業,請在呼叫 DataReader 的 Close 方法之前,先呼叫 Command 物件的 Cancel。呼叫 DataReader 的 Close 方法會導致它取得尚未處理的結果,並且在關閉游標之前清空資料流。呼叫 Command 的 Cancel 會放棄伺服器上的結果,因此 DataReader 就不用再讀取這些資料而可以直接進行關閉。如果您是透過 Command 的輸出參數傳回值,呼叫 Cancel 也會放棄它們。如果您需要讀取任何輸出參數,請不要呼叫 Command 的 Cancel 方法;只要呼叫 DataReader 的 Close 就好了。
二進位大型物件 (BLOB)
在使用 DataReader 取回二進位大型物件 (BLOB) 時,您需要在呼叫 ExecuteReader 方法時傳入 CommandBehavior.SequentialAccess。因為 DataReader 的預設行為是在執行每個 Read 方法時載入整個資料列到記憶體中,而因為 BLOB 值可能很大,結果可能會是每一個 BLOB 都使用到大量的記憶體。SequentialAccess 會將 DataReader 的行為指定只載入要求的資料。您可以利用 GetBytes 或是 GetChars 來控制一次載入多少資料。
請記住,當使用 SequentialAccess 時,您不能不按次序取用 DataReader 的各個欄位。也就是說,如果您的查詢要求三個欄位,第三個欄位是 BLOB,而您並不想要取用前兩個欄位的值,您必須先取用第一個欄位的值,然後是第二個欄位,才能夠取用 BLOB 的資料。這是因為現在資料是按照順序傳回,而一旦 DataReader 讀取過後就不能再回頭讀取了。
有關如何在 ADO.NET 中取用 BLOB 的資料,請參考 Obtaining BLOB Values from a Database 一文。
使用命令
ADO.NET 提供了幾種不同的方法進行命令的執行,也提供不同的選項來對命令的執行進行最佳化的處理。以下提供了一些選擇最佳命令執行方法,以及如何改善命令執行時的效能的秘訣。
OleDbCommand 的最佳經驗分享
在不同的 .NET Framework 資料提供者之間執行命令的方式已經儘可能的標準化。然而,不同的資料提供者之間還是會有一些差異。以下是一些在使用 .NET Framework Data Provider for OLE DB 時執行命令可以進行的微調作業。
- 利用 CommandType.Text 配合上 ODBC 的 CALL 語法來呼叫預存程序。利用 CommandType.StoredProcedure 會在私下產生 ODBC CALL 的語法。
- 請務必指定 OleDbParameter 的型別類型、大小(如果有的話)以及精確度和度量範圍(如果參數是數值或十進位型別類型時)。注意到,如果您不明確指定參數資訊,OleDbCommand 會在每一次執行命令時重新建立 OLE DB 參數存取元。
SqlCommand 的最佳經驗分享
利用 SqlCommand 執行預存程序的快速秘訣:如果您正呼叫預存程序,指定 SqlCommand 的 CommandType 屬性為 StoredProcedure。這會避開執行前對命令進行剖析作業的程序,明確的將它視為一個預存程序。
使用 Prepare 方法
Command.Prepare 方法可以增加對您的資料來源重複進行參數化命令的執行效能。Prepare 會指示資料來源對指定命令被重複呼叫做最佳化的處理。要有效使用 Prepare,您需要相當瞭解資料來源會如何回應對 Prepare 的呼叫。對於有些像 SQL Server 2000 這類的資料來源,命令會私下進行最佳化,所以呼叫 Prepare 是多餘的。至於其他像是 SQL Server 7.0 的資料來源,Prepare 就很有幫助了。
明確指定結構描述及中繼資料
在 ADO.NET 中的許多物件會在使用者未指示中繼資料的情況下自行推論。例如:
- DataAdapter.Fill 方法,它會在 DataSet 中沒有資料表的情況下產生一個資料表和資料欄位。
- CommandBuilder 會為單一資料表的 SELECT 命令產生 DataAdapter 命令屬性。
- CommandBuilder.DeriveParameters 方法會負責產生 Command 物件的 Parameters 集合物件。
不過每次使用到這些功能時,都會有效能上的影響。我們建議這些功能最好都用在設計時期或一般的應用程式。只要有可能,就明確指示結構描述和中繼資料。這包括了在 DataSet 中定義資料表和欄位,定義 DataAdapter 的 Command 屬性,以及定義 Command 的 Parameter 資訊。
ExecuteScalar 及 ExecuteNonQuery
如果您想要傳回單一的值,像是 Count(*)、Sum(Price) 或 Avg(Quantity) 的結果,您可以使用 Command.ExecuteScalar。 ExecuteScalar 傳回第一列第一欄的值,將結果視為一般的純量值。ExecuteScalar 一方面簡化您的程式並透過執行單一步驟來代替使用 DataReader 時要進行的兩個步驟 (也就是 ExecuteReader 再加上取得資料值) 以改善執行效能。
當使用到不傳回資料列的 SQL 敘述時,像是修改資料的命令 (例如 INSERT、UPDATE 或 DELETE) 或是只傳回輸出參數或傳回值時,請使用 ExecuteNonQuery。這樣會避開產生空的 DataReader 而來的多餘處理程序。
有關更進一步的資訊,請參考 Executing a Command 一文。
測試空值
如果您資料庫中的資料表的欄位允許空值,就無法測試參數值是否「等於」null。相對的,您需要設計一個 WHERE 子句來測試是欄位為空值或是參數為空值。以下的 SQL 敘述會傳回 LastName 欄位等於 @LastName 參數中的內容的資料列,或者是 LastName 欄位及 @LastName 參數都是空值。
SELECT * FROM Customers WHERE ((LastName = @LastName) OR (LastName IS NULL AND @LastName IS NULL))
在參數中傳遞空值
當在對資料庫的命令中傳入空值做為參數時,您不能使用 null (在Visual Basic® .NET中是 Nothing)。相對的您要使用 DBNull.Value。例如:
'Visual Basic Dim param As SqlParameter = New SqlParameter("@Name", SqlDbType.NVarChar, 20) param.Value = DBNull.Value //C# SqlParameter param = new SqlParameter("@Name", SqlDbType.NVarChar, 20); param.Value = DBNull.Value;
進行異動
在 ADO.NET 中的異動模型已經有所改變。在 ADO 中,當呼叫到 StartTransaction 時,任何在這個呼叫之後的更新都會成為異動的一部份。然而,在 ADO.NET 中,當呼叫到 Connection.BeginTransaction 時,會傳回一個 Transaction 物件,它必須要和一個 Command 物件的 Transaction 屬性建立關聯。這個設計讓您能在單一的連接上進行多個根源的異動作業。如果 Command.Transaction 屬性並未指定到相關 Connection 的 Transaction 物件,Command 就會失敗並丟出一個例外。
在後續推出的 .NET Framework 版本中會讓您能自行列舉處理既有分散式異動。這很適合用在有物件集區的情況,對於一個集區中的物件只會開啟一次連接,但是物件卻會牽涉到多個異動。在 .NET Framework 1.0 中並不提供這個能力。
有關異動這部份更進一步的資訊,請參考 Performing Transactions 和 .NET Data Access Architecture Guide等文件。
使用資料連接
高執行效能的應用程式會讓使用中對資料來源的連接儘可能的保持在最少的時間,並善用像連接共用 (connection pooling) 這類技巧來改善執行效能。以下的主題提供您在使用 ADO.NET 連接到資料來源時,達成最大效能的一些秘訣。
連接共用
SQL Server、OLE DB 和 .NET Framework Data Provider for ODBC 會私下將連接安排到集區中共用。您可以藉由在連接字串中指定不同的屬性值來控制連接共用的行為。有關如何控制連接共用的行為的相關細節,請參考 Connection Pooling for the SQL Server .NET Data Provider 和 Connection Pooling for the OLE DB .NET Data Provider 等文的說明。
利用 DataAdapter 最佳化連接的使用
DataAdapter 的 Fill 及 Update 方法會在連接已關閉的情況下,自動開啟相關命令屬性所指定的連接。如果 Fill 或 Update 方法開啟了連接,Fill 或 Update 會在作業完成時關掉它。要取得最佳效能,只要在必要時才保留對資料庫的連接。同時,也降低您開啟及關閉連接進行多個作業的次數。
我們的建議是如果您只是要進行單一的 Fill 或 Update 方法,您可以讓 Fill 或 Update 方法私下自行開啟及關閉連結。如果您是要對 Fill 或 Update 進行多次的呼叫,建議您自行明確的開啟連結,呼叫 Fill 及 Update,然後明確的關閉連接。
更進一步說,在進行異動時,要於異動進行前明確的開啟連接,並在完成異動後關閉連接。例如:
'Visual Basic Public Sub RunSqlTransaction(da As SqlDataAdapter, myConnection As SqlConnection, ds As DataSet) myConnection.Open() Dim myTrans As SqlTransaction = myConnection.BeginTransaction() myCommand.Transaction = myTrans Try da.Update(ds) myTrans.Commit() Console.WriteLine("Update successful.") Catch e As Exception Try myTrans.Rollback() Catch ex As SqlException If Not myTrans.Connection Is Nothing Then Console.WriteLine("An exception of type " & ex.GetType().ToString() & _ " was encountered while attempting to roll back the transaction.") End If End Try Console.WriteLine("An exception of type " & e.GetType().ToString() & " was encountered.") Console.WriteLine("Update failed.") End Try myConnection.Close() End Sub //C# public void RunSqlTransaction(SqlDataAdapter da, SqlConnection myConnection, DataSet ds) { myConnection.Open(); SqlTransaction myTrans = myConnection.BeginTransaction(); myCommand.Transaction = myTrans; try { da.Update(ds); myCommand.Transaction.Commit(); Console.WriteLine("Update successful."); } catch(Exception e) { try { myTrans.Rollback(); } catch (SqlException ex) { if (myTrans.Connection != null) { Console.WriteLine("An exception of type " + ex.GetType() + " was encountered while attempting to roll back the transaction."); } } Console.WriteLine(e.ToString()); Console.WriteLine("Update failed."); } myConnection.Close(); }
務必關閉連接和 DataReader
務必要在使用完 Connection 或 DataReader 物件時明確的將其關閉。資源回收的作業是在清除物件,並釋放連接及其他受管理的資源時,但資源回收的動作只會在必要時進行。因此,您依舊有責任確認所有昂貴的資源都是明確的被釋放掉。同時,未明確關閉的Connections 可能不會被歸還到集區中。例如,已經離開適用範圍的連接並未被明確關閉,這時只有在已經達到最大的集區大小而連接依舊適用時會被歸還回集區。
注意 不要在類別中的 Finalize 方法中呼叫 Connection、DataReader 或是任何其他受管理物件的 Close 或 Dispose 方法。在一個完成設定式 (Finalizer) 中,只能釋放您所直接控管的未受管理資源。如果您的類別並不擁有任何未受管理資源,就不要在您的類別定義中安排 Finalize 方法。
在 C# 中使用「using」敘述
對於 C# 開發人員,確保您總是會關閉 Connection 和 DataReader 物件的一個便利方式是使用 using 敘述。using 敘述會在所使用的物件離開 using 敘述的適用範圍時自動呼叫物件的 Dispose 方法。例如:
//C# string connString = "Data Source=localhost;Integrated Security=SSPI;Initial Catalog=Northwind;"; using (SqlConnection conn = new SqlConnection(connString)) { SqlCommand cmd = conn.CreateCommand(); cmd.CommandText = "SELECT CustomerId, CompanyName FROM Customers"; conn.Open(); using (SqlDataReader dr = cmd.ExecuteReader()) { while (dr.Read()) Console.WriteLine("{0}\t{1}", dr.GetString(0), dr.GetString(1)); } }
using 敘述不適用在 Microsoft® Visual Basic® .NET 中。
避免存取 OleDbConnection.State 屬性
如果連接已經開啟,OleDbConnection.State 屬性會透過原生的 OLE DB 函式 IDBProperties.GetProperties 來叫用 DATASOURCEINFO 屬性,以指定 DBPROP_CONNECTIONSTATUS 屬性的值,這可能牽涉到對資料來源的來回作業。換句話說,檢查 State 屬性可能很昂貴。因此只要在必要時才檢查 State 屬性。如果您需要經常檢查這個屬性,您的應用程式不妨去注意 OleDbConnection 的 StateChange 事件,效益會更好。有關 StateChange 事件的細節,請參考 Working with Connection Events 一文。
和 XML 的整合
ADO.NET 在 DataSet 中充份的整合了 XML 的處理能力,也提供了一些由 SQL Server 2000 及後續版本所支援的 XML 功能。您也可以使用 SQLXML 3.0 來取用 SQL Server 2000 及後續版本中的 XML 功能。下面是在使用 XML 和 ADO.NET 的相關秘訣及資訊。
DataSet 和 XML
DataSet 緊密結合了 XML,讓您能夠:
- 依據 XSD Schema 載入 DataSet 的結構描述和關聯架構。
- 依據 XML 載入一個 DataSet 的內容。
- 當未提供結構描述時,從 XML 文件的內容推論 DataSet 的結構描述。
- 將一個 DataSet 的結構描述寫出成 XSD Schema 的格式。
- 將一個 DataSet 的內容表示成 XML。
- 同步存取利用 DataSet所表現的資料的關聯,以及利用 XmlDataDocument 所表現的資料階層關係。
注意 您可以利用這個同步作業對 DataSet 中的資料套用 XML 的功能,像是 XPath 查詢,以及 XSLT 轉換,或是檢視 XML 文件中所有資料或部份資料間的關係,同時還保有原始 XML 的精確性。
有關 DataSet 所提供 XML 的功能細節,請參考 XML and the DataSet。
結構描述的推論
當從一個 XML 檔載入一個 DataSet 時,您可以從 XSD Schema 載入 DataSet 的結構描述,或是在載入資料之前預先定義資料表和欄位。如果沒有可用的 XSD Schema,您也不知道在 XML 檔中定義了那些資料表和欄位,您可以依據 XML 文件的內容來推論結構描述。
結構推論是很適合用在轉移作業上的工具,但最好限制在設計時期的應用程式,因為推論的處理程序有以下的限制。
- 推論結構描述會產生額外的作業程序,影響到應用程式的執行效能。
- 所有推論的欄位都是字串型別。
- 推論的程序不是決定性的。也就是說,它是依據 XML 檔案的內容來進行,而不是依預期的結構描述進行。結果是,您可能有兩份 XML 檔案都依據同樣的結構描述產生,但是產生的推論結果卻不一樣,因為它們兩份檔案的實際內容不同。
相關的資訊請參考 Inferring DataSet Relational Structure from XML。
SQL Server For XML 查詢
如果您傳回的是 SQL Server 2000 FOR XML 查詢的結果,您可以利用 SqlCommand.ExecuteXmlReader 方法讓 .NET Framework Data Provider for SQL Server 直接產生一個 XmlReader。
SQLXML 受管理的類別
在 .NET Framework 中安排了一些類別來提供 XML for SQL Server 2000 的功能。這些類別可以在 Microsoft.Data.SqlXml 命名空間中找到,並且還加上執行 XPath 查詢及 XML 範本檔的功能,以及套用 XSLT 轉換到資料上的能力。
SQLXML 受管理類別包含在 XML for Microsoft SQL Server 2000 (SQLXML 2.0)中,可以透過 XML for Microsoft SQL Server 2000 Web Release 2 (SQLXML 2.0) 的超連結取得。
更多有用秘訣
以下是撰寫 ADO.NET 程式碼一些常用的秘訣。
避免自動增量值的衝突
如同大多數的資料來源,DataSet 讓您能夠決定在新資料列加入時自動增量的欄位。當在 DataSet 中使用來自資料來源的自動增量欄位時,要避免在本地新加入 DataSet 資料列的編號和加入到資料來源中的資料列的編號衝突。
例如,考慮一個具有自動增量欄位 CustomerID 的資料表。有兩個新的客戶紀錄加入到資料表中,並且取得自動增量的 CustomerID 值,分別為 1 及 2。然後,只有第二個客戶的資料列傳給 DataAdapter 的 Update 方法,新加入的紀錄在資料來源中取得一個自動增量的 CustomerID,值為 1,而不是 DataSet中的 2。當 DataAdapter 以傳回的值填入資料表中的第二筆紀錄時,就會發生限制衝突,因為第一筆客戶的資料列已經有一個 CustomerID 的值為 1。
要避免這樣的情況,建議的方式是在處理資料來源和 DataSet 的自動增量欄位時,您會在 DataSet 中建立一個欄位的 AutoIncrementStep 值為 -1,而 AutoIncrementSeed 的值為 0,同時也確認您的資料來源所產生的自動增量值是從 1 開始,同時向正向增加。結果是 DataSet 會產生負數的增量值,而不會和資料來源所產生的正向增量值相衝突。另外一個選擇是使用型別為 Guid 的欄位代替自動增量的欄位。產生 Guid 值的演算法絕不會在 DataSet 和資料來源中產生出重複的 Guid 值。
如果您的自動增量值只是用來做為唯一的值,而沒有任何額外的用意,請考慮採用 Guids 來代替自動增量欄位。它們會是唯一的,同時避免面對自動增量欄位時額外的作業。
有關取用資料來源中自動增量欄位的內容相關的資訊,請參考 Retrieving Identity or AutoNumber Values。
檢查最佳化並行衝突
由於 DataSet 是設計來和資料來源以離線的方式作業,您需要確認您的應用程式可以在使用樂觀並行模型的情況下,避免多個前端去更新資料來源時可能發生的衝突。
有不少技巧可以用來測試樂觀並行衝突的情況。其中一個是在資料表中使用了郵戮欄位。另一個技巧是藉由在您的 SQL 敘述中使用一個 WHERE 子句來驗證所有資料列中原始的欄位可以吻合在資料庫中對應的項目。
有關這個主題的相關資訊及程式範例,請參考 Optimistic Concurrency 一文。
多執行緒程式設計
ADO.NET 設計時主要考量到執行效能、產能以及延展能力。因此,ADO.NET 的物件不會鎖定資源,也只能用帶單一執行緒的情況。唯一的例外是 DataSet,它在多個讀取器的情況下是執行緒安全的。然而,您需要在寫入時鎖定 DataSet。
只在必要時利用 COM Interop 取用 ADO
ADO.NET 是設計來做為許多應用程式的最佳解決方案。然而,有些應用程式所需要的功能只在 ADO 物件上提供,像是 ADO 多維物件 (ADOMD)。在這些情況下,應用程式可以利用 COM Interop 的技術存取 ADO 物件。注意到使用 COM Interop 來存取 ADO 處理資料會產生執行效能上的影響。當設計一個應用程式而打算採用 COM Interop 取用 ADO 的功能前,要先判斷 ADO.NET 是否就吻合您設計上的需要。
本分文件所提供的資訊代表的是微軟公司在出版這份文件當時對所討論議題的觀點。由於微軟公司必須要對市場的改變做出反應,因此這並不代表微軟公司所提出的承諾的一部份,而微軟公司也無法保證在出版日期後資訊的正確性。
本白皮書僅供參考之用。MICROSOFT 並未對本文件的資訊做任何明示或默示的保證。
使用者有責任遵守所有應當遵守之智慧財產法。本文件包含所有受到版權保護的所有資訊,未經 Microsoft 公司的書面許可,嚴禁以任何形式、方法 (包括電子、機械、影印或其他方法),或出於任何目的,複製、保存或輸入於檢索系統 (a retrieval system)。
Microsoft 可能擁有本文件所提及內容中所含之專利權、專利優先權、商標、著作權,或其他智慧財產權。除非 Microsoft 書面授權合約所明示規定者外,提供本文件並不授予 貴用戶上述專利權、商標、著作權或其他智慧財產權。
© 2002 Microsoft Corporation 著作權所有,並保留一切權利。
Microsoft, Visual Basic 及 Visual Studio 係 Microsoft Corporation 在美國及/或其他國家的註冊商標或商標。
本文件所提及之產品或公司的實際名稱可能為各擁有人所有之商標。