本文為您提供了在 Microsoft ADO.NET 應(yīng)用程序中實現(xiàn)和獲得最佳性能、可伸縮性以及功能的最佳解決方案;同時也講述了使用 ADO.NET 中可用對象的最佳實踐;并提出一些有助于優(yōu)化 ADO.NET 應(yīng)用程序設(shè)計的建議。
本文包含:
• | 有關(guān) .NET 框架包含的 .NET 框架數(shù)據(jù)提供程序的信息。 |
• | DataSet 和 DataReader 之間的比較,以及這些對象中每個對象最佳用法的解釋。 |
• | 解釋如何使用 DataSet、Commands 和 Connections。 |
• | 有關(guān)與 XML 集成的信息。 |
• | 通用的技巧和問題。 |
ADO.NET 提供以下兩個對象,用于檢索關(guān)系數(shù)據(jù)并將其存儲在內(nèi)存中:DataSet 和 DataReader。DataSet 提供一個內(nèi)存中數(shù)據(jù)的關(guān)系表示形式,一整套包括一些表在內(nèi)的數(shù)據(jù)(這些表包含數(shù)據(jù)、對數(shù)據(jù)進行排序并約束數(shù)據(jù)),以及表之間的關(guān)系。DataReader 提供一個來自數(shù)據(jù)庫的快速、只進、只讀數(shù)據(jù)流。
當(dāng)使用 DataSet 時,經(jīng)常會利用 DataAdapter(也可能是 CommandBuilder)與數(shù)據(jù)源進行交互。當(dāng)使用 DataSet 時,也可以利用 DataView 對 DataSet 中的數(shù)據(jù)應(yīng)用排序和篩選。也可以從 DataSet 繼承,創(chuàng)建強類型 DataSet,用于將表、行和列作為強類型對象屬性公開。
下列主題包括的信息涉及:使用 DataSet 或 DataReader 的最佳時機、如何優(yōu)化訪問它們所包含數(shù)據(jù)、以及如何優(yōu)化使用 DataAdapter(包括 CommandBuilder)和 DataView 的技巧。
DataSet 與 DataReader
當(dāng)設(shè)計應(yīng)用程序時,要考慮應(yīng)用程序所需功能的等級,以確定使用 DataSet 或者是 DataReader。
要通過應(yīng)用程序執(zhí)行以下操作,就要使用 DataSet:
• | 在結(jié)果的多個離散表之間進行導(dǎo)航。 |
• | 操作來自多個數(shù)據(jù)源(例如,來自多個數(shù)據(jù)庫、一個 XML 文件和一個電子表格的混合數(shù)據(jù))的數(shù)據(jù)。 |
• | 在各層之間交換數(shù)據(jù)或使用 XML Web 服務(wù)。與 DataReader 不同的是,DataSet 能傳遞給遠程客戶端。 |
• | 重用同樣的行組,以便通過緩存獲得性能改善(例如排序、搜索或篩選數(shù)據(jù))。 |
• | 每行執(zhí)行大量處理。對使用 DataReader 返回的每一行進行擴展處理會延長服務(wù)于 DataReader 的連接的必要時間,這影響了性能。 |
• | 使用 XML 操作對數(shù)據(jù)進行操作,例如可擴展樣式表語言轉(zhuǎn)換(XSLT 轉(zhuǎn)換)或 XPath 查詢。 |
對于下列情況,要在應(yīng)用程序中使用 DataReader:
• | 不需要緩存數(shù)據(jù)。 |
• | 要處理的結(jié)果集太大,內(nèi)存中放不下。 |
• | 一旦需要以只進、只讀方式快速訪問數(shù)據(jù)。 |
注填充 DataSet 時,DataAdapter 使用 DataReader。因此,使用 DataAdapter 取代 DataSet 提升的性能表現(xiàn)為節(jié)省了 DataSet 占用內(nèi)存和填充 DataSet 需要的循環(huán)。一般來說,此性能提升只是象征性的,因此,設(shè)計決策應(yīng)以所需功能為基礎(chǔ)。
使用強類型 DataSet 的好處
DataSet 的另一個好處是可被繼承以創(chuàng)建一個強類型 DataSet。強類型 DataSet 的好處包括設(shè)計時類型檢查,以及 Microsoft Visual Studio .NET 用于強類型 DataSet 語句結(jié)束所帶來的好處。修改了 DataSet 的架構(gòu)或關(guān)系結(jié)構(gòu)后,就可以創(chuàng)建一個強類型 DataSet,把行和列作為對象的屬性公開,而不是作為集合中的項公開。例如,不公開客戶表中行的姓名列,而公開 Customer 對象的 Name 屬性。類型化 DataSet 從 DataSet 類派生,因此不會犧牲 DataSet 的任何功能。也就是說,類型化 DataSet 仍能遠程訪問,并作為數(shù)據(jù)綁定控件(例如 DataGrid)的數(shù)據(jù)源提供。如果架構(gòu)事先不可知,仍能受益于通用 DataSet 的功能,但卻不能受益于強類型 DataSet 的附加功能。
處理強類型 DataSet 中的空引用
使用強類型 DataSet 時,可以批注 DataSet 的 XML 架構(gòu)定義語言 (XSD) 架構(gòu),以確保強類型 DataSet 正確處理空引用。nullValue 批注使您可用一個指定的值 String.Empty 代替 DBNull、保留空引用或引發(fā)異常。選擇哪個選項取決于應(yīng)用程序的上下文。默認(rèn)情況下,如果遇到空引用,就會引發(fā)異常。
有關(guān)更多信息,請參閱 Working with a Typed DataSet。
刷新 DataSet 中的數(shù)據(jù)
如果想用服務(wù)器上的更新值刷新 DataSet 中的值,就使用 DataAdapter.Fill。如果有在 DataTable 上定義的主鍵,DataAdapter.Fill 會根據(jù)主鍵進行新行匹配,并且當(dāng)更改到現(xiàn)有行時應(yīng)用服務(wù)器上的值。即使刷新之前修改了它們,刷新行的 RowState 仍被設(shè)置為 Unchanged。注意,如果沒有為 DataTable 定義主鍵,DataAdapter.Fill 就用可能重復(fù)的主鍵值添加新行。
如果想用來自服務(wù)器的當(dāng)前值刷新表,并同時保留對表中的行所做的任何更改,必須首先用 DataAdapter.Fill 填充表,并填充一個新的 DataTable,然后用 preserveChanges 值 true 把 DataTableMerge 到 DataSet 中。
在 DataSet 中搜索數(shù)據(jù)
在 DataSet 中查詢與特定條件相匹配的行時,可以利用基于索引的查找提高搜索性能。當(dāng)把 PrimaryKey 值賦給 DataTable 時,會創(chuàng)建一個索引。當(dāng)給 DataTable 創(chuàng)建 DataView 時,也會創(chuàng)建一個索引。下面是一些利用基于索引進行查找的技巧。
• | 如果對組成 DataTable 的 PrimaryKey的列進行查詢,要使用 DataTable.Rows.Find 而不是 DataTable.Select。 |
• | 對于涉及到非主鍵列的查詢,可以使用 DataView 為數(shù)據(jù)的多個查詢提高性能。當(dāng)把排序順序應(yīng)用到 DataView 時,就會建立一個搜索時使用的索引。DataView 公開 Find 和 FindRows 方法,以便查詢基礎(chǔ) DataTable 中的數(shù)據(jù)。 |
• | 如果不需要表的排序視圖,仍可以通過為 DataTable 創(chuàng)建 DataView 來利用基于索引的查找。注意,只有對數(shù)據(jù)執(zhí)行多個查詢操作時,這樣才會帶來好處。如果只執(zhí)行單一查詢,創(chuàng)建索引所需要的處理就會降低使用索引所帶來的性能提升。 |
DataView 構(gòu)造
如果創(chuàng)建了 DataView,并且修改了 Sort、RowFilter 或 RowStateFilter 屬性,DataView 就會為基礎(chǔ) DataTable 中的數(shù)據(jù)建立索引。創(chuàng)建 DataView 對象時,要使用 DataView 構(gòu)造函數(shù),它用 Sort、RowFilter 和 RowStateFilter 值作為構(gòu)造函數(shù)參數(shù)(與基礎(chǔ) DataTable 一起)。結(jié)果是創(chuàng)建了一次索引。創(chuàng)建一個“空”DataView 并隨后設(shè)置 Sort、RowFilter 或 RowStateFilter 屬性,會導(dǎo)致索引至少創(chuàng)建兩次。
分頁
ADO.NET 可以顯式控制從數(shù)據(jù)源中返回什么樣的數(shù)據(jù),以及在 DataSet 中本地緩存多少數(shù)據(jù)。對查詢結(jié)果的分頁沒有唯一的答案,但下面有一些設(shè)計應(yīng)用程序時應(yīng)該考慮的技巧。
• | 避免使用帶有 startRecord 和 maxRecords 值的 DataAdapter.Fill 重載。當(dāng)以這種方式填充 DataSet 時,只有 maxRecords 參數(shù)(從 startRecord 參數(shù)標(biāo)識的記錄開始)指定的記錄數(shù)量用于填充 DataSet,但無論如何總是返回完整的查詢。這就會引起不必要的處理,用于讀取“不需要的”記錄;而且為了返回附加記錄,會耗盡不必要的服務(wù)器資源。 |
• | 用于每次只返回一頁記錄的技術(shù)是創(chuàng)建 SQL 語句,把 WHERE 子句以及 ORDER BY 子句和 TOP 謂詞組合起來。此技術(shù)取決于存在一種可唯一標(biāo)識每一行的辦法。當(dāng)瀏覽下一頁記錄時,修改 WHERE 子句使之包含所有唯一標(biāo)識符大于當(dāng)前頁最后一個唯一標(biāo)識符的記錄。當(dāng)瀏覽上一頁記錄時,修改 WHERE 子句使之返回所有唯一標(biāo)識符小于當(dāng)前頁第一個唯一標(biāo)識符的記錄。兩種查詢都只返回記錄的 TOP 頁。當(dāng)瀏覽上一頁時,需要以降序為結(jié)果排序。這將有效地返回查詢的最后一頁(如果需要,顯示之前也許要重新排序結(jié)果)。有關(guān)這個技術(shù)的一個示例,請參閱 Paging Through a Query Result。 |
• | 另一項每次只返回一頁記錄的技術(shù)是創(chuàng)建 SQL 語句,把 TOP 謂詞和嵌入式 SELECT 語句的使用結(jié)合在一起。此技術(shù)并不依賴于存在一種可唯一標(biāo)識每一行的辦法。使用這項技術(shù)的第一步是把所需頁的數(shù)量與頁大小相乘。然后將結(jié)果傳遞給 SQL Query 的 TOP 謂詞,該查詢以升序排列。再把此查詢嵌入到另一個查詢中,后者從降序排列的嵌入式查詢結(jié)果中選擇 TOP 頁大小。實質(zhì)上,返回的是嵌入式查詢的最后一頁。例如,要返回查詢結(jié)果的第三頁(頁大小是 10),應(yīng)該書寫如下所示的命令: SELECT TOP 10 * FROM (SELECT TOP 30 * FROM Customers ORDER BY Id ASC) AS Table1ORDER BY Id DESC 注意,從查詢中返回的結(jié)果頁以降序顯示。如果需要,應(yīng)該重新排序。 |
• | 如果數(shù)據(jù)不經(jīng)常變動,可以在 DataSet 中本地維護一個記錄緩存,以此提高性能。例如,可以在本地 DataSet 中存儲 10 頁有用的數(shù)據(jù),并且只有當(dāng)用戶瀏覽超出緩存第一頁和最后一頁時,才從數(shù)據(jù)源中查詢新數(shù)據(jù)。 |
有關(guān)更多信息,請參閱 .NET Data Access Architecture Guide。
用架構(gòu)填充 DataSet
當(dāng)用數(shù)據(jù)填充 DataSet 時,DataAdapter.Fill 方法使用 DataSet 的現(xiàn)有架構(gòu),并使用從 SelectCommand 返回的數(shù)據(jù)填充它。如果在 DataSet 中沒有表名與要被填充的表名相匹配,Fill 方法就會創(chuàng)建一個表。默認(rèn)情況下,Fill 僅定義列和列類型。
通過設(shè)置 DataAdapter 的 MissingSchemaAction 屬性,可以重寫 Fill 的默認(rèn)行為。例如,要讓 Fill 創(chuàng)建一個表架構(gòu),并且還包括主鍵信息、唯一約束、列屬性、是否允許為空、最大列長度、只讀列和自動增量的列,就要把 DataAdapter.MissingSchemaAction 指定為 MissingSchemaAction.AddWithKey?;蛘?,在調(diào)用 DataAdapter.Fill 前,可以調(diào)用 DataAdapter.FillSchema 來確保當(dāng)填充 DataSet 時架構(gòu)已到位。
對 FillSchema 的調(diào)用會產(chǎn)生一個到服務(wù)器的額外行程,用于檢索附加架構(gòu)信息。為了獲得最佳性能,需要在調(diào)用 Fill 之前指定 DataSet 的架構(gòu),或者設(shè)置 DataAdapter 的 MissingSchemaAction。
使用 CommandBuilder 的最佳實踐
假設(shè) SelectCommand 執(zhí)行單一表 SELECT,CommandBuilder 就會以 DataAdapter 的 SelectCommand 屬性為基礎(chǔ)自動生成 DataAdapter 的 InsertCommand、UpdateCommand、和 DeleteCommand 屬性。下面是為獲得最佳性能而使用 CommandBuilder 的一些技巧。
• | CommandBuilder 的使用應(yīng)該限制在設(shè)計時或即席方案中。生成 DataAdapter 命令屬性所必需的處理會影響性能。如果預(yù)先知道 INSERT/UPDATE/DELETE 語句的內(nèi)容,就顯式設(shè)置它們。一個比較好的設(shè)計技巧是,為 INSERT/UPDATE/DELETE 命令創(chuàng)建存儲過程并顯式配置 DataAdapter 命令屬性以使用它們。 |
• | CommandBuilder 使用 DataAdapter 的 SelectCommand 屬性確定其他命令屬性的值。如果 DataAdapter 的 SelectCommand 本身曾經(jīng)更改過,確保調(diào)用 RefreshSchema 以更新命令屬性。 |
• | 如果 DataAdapter 命令屬性為空(命令屬性默認(rèn)情況下為空),CommandBuilder 僅僅為它生成一條命令。如果顯式設(shè)置了命令屬性,CommandBuilder 不會重寫它。如果希望 CommandBuilder 為以前已經(jīng)設(shè)置過的命令屬性生成命令,就把命令屬性設(shè)置為空。 |
批處理 SQL 語句
很多數(shù)據(jù)庫支持把多條命令合并或批處理成一條單一命令執(zhí)行。例如,SQL Server 使您可以用分號 (;) 分隔命令。把多條命令合并成單一命令,能減少到服務(wù)器的行程數(shù),并提高應(yīng)用程序的性能。例如,可以把所有預(yù)定的刪除在應(yīng)用程序中本地存儲起來,然后再發(fā)出一條批處理命令調(diào)用,從數(shù)據(jù)源刪除它們。
雖然這樣做確實能提高性能,但是,當(dāng)對 DataSet 中的數(shù)據(jù)更新進行管理時,可能會增加應(yīng)用程序的復(fù)雜性。要保持簡單,可能要在 DataSet 中為每個 DataTable 創(chuàng)建一個 DataAdapter。
用多個表填充 DataSet
如果使用批處理 SQL 語句檢索多個表并填充 DataSet,第一個表用指定給 Fill 方法的表名命名。后面的表用指定給 Fill 方法的表名加上一個從 1 開始并且增量為 1 的數(shù)字命名。例如,如果運行下面的代碼:
‘Visual BasicDim 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 表的數(shù)據(jù)放在名為 "Customers" 的 DataTable 中。來自 Orders 表的數(shù)據(jù)放在名為 "Customers1" 的 DataTable 中。
填充完 DataSet 之后,可以很容易地把 "Customers1" 表的 TableName 屬性改為 "Orders"。但是,后面的填充會導(dǎo)致 "Customers" 表被重新填充,而 "Orders" 表會被忽略,并創(chuàng)建另外一個 "Customers1" 表。為了對這種情況作出補救,創(chuàng)建一個 DataTableMapping,把 "Customers1" 映射到 "Orders",并為其他后面的表創(chuàng)建其他的表映射。例如:
‘Visual BasicDim 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 獲得最佳性能的技巧,同時還回答了一些關(guān)于使用 DataReader 的常見問題。
• | 在訪問相關(guān) Command 的任何輸出參數(shù)之前,必須關(guān)閉 DataReader。 |
• | 完成讀數(shù)據(jù)之后總是要關(guān)閉 DataReader。如果使用 Connection 只是用于返回 DataReader,那么關(guān)閉 DataReader 之后立刻關(guān)閉它。 另外一個顯式關(guān)閉 Connection 的方法是把 CommandBehavior.CloseConnection 傳遞給 ExecuteReader 方法,以確保相關(guān)的連接在關(guān)閉 DataReader 時被關(guān)閉。如果從一個方法返回 DataReader,而且不能控制 DataReader 或相關(guān)連接的關(guān)閉,則這樣做特別有用。 |
• | 不能在層之間遠程訪問 DataReader。DataReader 是為已連接好的數(shù)據(jù)訪問設(shè)計的。 |
• | 當(dāng)訪問列數(shù)據(jù)時,使用類型化訪問器,例如,GetString、GetInt32 等。這使您不用進行將 GetValue 返回的 Object 強制轉(zhuǎn)換成特定類型所需的處理。 |
• | 一個單一連接每次只能打開一個 DataReader。在 ADO 中,如果打開一個單一連接,并且請求兩個使用只進、只讀游標(biāo)的記錄集,那么 ADO 會在游標(biāo)生存期內(nèi)隱式打開第二個、未池化的到數(shù)據(jù)存儲區(qū)的連接,然后再隱式關(guān)閉該連接。對于 ADO.NET,“秘密”完成的動作很少。如果想在相同的數(shù)據(jù)存儲區(qū)上同時打開兩個 DataReaders,就必須顯式創(chuàng)建兩個連接,每個 DataReader 一個。這是 ADO.NET 為池化連接的使用提供更多控制的一種方法。 |
• | 默認(rèn)情況下,DataReader 每次 Read 時都要把整行加載到內(nèi)存。這允許在當(dāng)前行內(nèi)隨機訪問列。如果不需要這種隨機訪問,為了提高性能,就把 CommandBehavior.SequentialAccess 傳遞給 ExecuteReader 調(diào)用。這將 DataReader 的默認(rèn)行為更改為僅在請求時將數(shù)據(jù)加載到內(nèi)存。注意,CommandBehavior.SequentialAccess 要求順序訪問返回的列。也就是說,一旦讀過返回的列,就不能再讀它的值了。 |
• | 如果已經(jīng)完成讀取來自 DataReader 的數(shù)據(jù),但仍然有大量掛起的未讀結(jié)果,就在調(diào)用 DataReader 的 Close 之前先調(diào)用 Command 的 Cancel。調(diào)用 DataReader 的 Close 會導(dǎo)致在關(guān)閉游標(biāo)之前檢索掛起的結(jié)果并清空流。調(diào)用 Command 的 Cancel 會放棄服務(wù)器上的結(jié)果,這樣,DataReader 在關(guān)閉的時候就不必讀這些結(jié)果。如果要從 Command 返回輸出參數(shù),還要調(diào)用 Cancel 放棄它們。如果需要讀取任何輸出參數(shù),不要調(diào)用 Command 的 Cancel,只要調(diào)用 DataReader 的 Close 即可。 |
二進制大對象 (BLOB)
用 DataReader 檢索二進制大對象 (BLOB) 時,應(yīng)該把 CommandBehavior.SequentialAccess 傳遞給 ExecuteReader 方法調(diào)用。因為 DataReader 的默認(rèn)行為是每次 Read 都把整行加載到內(nèi)存,又因為 BLOB 值可能非常大,所以結(jié)果可能由于單個 BLOB 而使大量內(nèi)存被用光。SequentialAccess 將 DataReader 的行為設(shè)置為只加載請求的數(shù)據(jù)。然后還可以使用 GetBytes 或 GetChars 控制每次加載多少數(shù)據(jù)。
記住,使用 SequentialAccess 時,不能不按順序訪問 DataReader 返回的不同字段。也就是說,如果查詢返回三列,其中第三列是 BLOB,并且想訪問前兩列中的數(shù)據(jù),就必須在訪問 BLOB 數(shù)據(jù)之前先訪問第一列的值,然后訪問第二列的值。這是因為現(xiàn)在數(shù)據(jù)是順序返回的,并且 DataReader 一旦讀過該數(shù)據(jù),該數(shù)據(jù)就不再可用。
有關(guān)如何在 ADO.NET 中訪問 BLOB 的詳細(xì)描述,請參閱 Obtaining BLOB Values from a Database。
ADO.NET 提供了幾種命令執(zhí)行的不同方法以及優(yōu)化命令執(zhí)行的不同選項。下面包括一些技巧,它們是關(guān)于選擇最佳命令執(zhí)行以及如何提高執(zhí)行命令的性能。
使用 OleDbCommand 的最佳實踐
不同 .NET 框架數(shù)據(jù)提供程序之間的命令執(zhí)行被盡可能標(biāo)準(zhǔn)化了。但是,數(shù)據(jù)提供程序之間仍然存在差異。下面給出一些技巧,可微調(diào)用于 OLE DB 的 .NET 框架數(shù)據(jù)提供程序的命令執(zhí)行。
• | 按照 ODBC CALL 語法使用 CommandType.Text 調(diào)用存儲過程。使用 CommandType.StoredProcedure 只是秘密地生成 ODBC CALL 語法。 |
• | 一定要設(shè)置 OleDbParameter 的類型、大?。ㄈ绻m用)、以及精度和范圍(如果參數(shù)類型是 numeric 或 decimal)。注意,如果不顯式提供參數(shù)信息,OleDbCommand 會為每個執(zhí)行命令重新創(chuàng)建 OLE DB 參數(shù)訪問器。 |
使用 SqlCommand 的最佳實踐
使用 SqlCommand 執(zhí)行存儲過程的快速提示:如果調(diào)用存儲過程,將 SqlCommand 的 CommandType 屬性指定為 StoredProcedure 的 CommandType。這樣通過將該命令顯式標(biāo)識為存儲過程,就不需要在執(zhí)行之前分析命令。
使用 Prepare 方法
對于重復(fù)作用于數(shù)據(jù)源的參數(shù)化命令,Command.Prepare 方法能提高性能。Prepare 指示數(shù)據(jù)源為多次調(diào)用優(yōu)化指定的命令。要想有效利用 Prepare,需要徹底理解數(shù)據(jù)源是如何響應(yīng) Prepare 調(diào)用的。對于一些數(shù)據(jù)源(例如 SQL Server 2000),命令是隱式優(yōu)化的,不必調(diào)用 Prepare。對于其他(例如 SQL Server 7.0)數(shù)據(jù)源,Prepare 會比較有效。
顯式指定架構(gòu)和元數(shù)據(jù)
只要用戶沒有指定元數(shù)據(jù)信息,ADO.NET 的許多對象就會推斷元數(shù)據(jù)信息。下面是一些示例:
• | DataAdapter.Fill 方法,如果 DataSet 中沒有表和列,DataAdapter.Fill 方法會在 DataSet 中創(chuàng)建表和列。 |
• | CommandBuilder,它會為單表 SELECT 命令生成 DataAdapter 命令屬性。 |
• | CommandBuilder.DeriveParameters,它會填充 Command 對象的 Parameters 集合。 |
但是,每次用到這些特性,都會有性能損失。建議將這些特性主要用于設(shè)計時和即席應(yīng)用程序中。在可能的情況下,顯式指定架構(gòu)和元數(shù)據(jù)。其中包括在 DataSet 中定義表和列、定義 DataAdapter 的 Command 屬性、以及為 Command 定義 Parameter 信息。
ExecuteScalar 和 ExecuteNonQuery
如果想返回像 Count(*)、Sum(Price) 或 Avg(Quantity) 的結(jié)果那樣的單值,可以使用 Command.ExecuteScalar。ExecuteScalar 返回第一行第一列的值,將結(jié)果集作為標(biāo)量值返回。因為單獨一步就能完成,所以 ExecuteScalar 不僅簡化了代碼,還提高了性能;要是使用 DataReader 就需要兩步才能完成(即,ExecuteReader + 取值)。
使用不返回行的 SQL 語句時,例如修改數(shù)據(jù)(例如INSERT、UPDATE 或 DELETE)或僅返回輸出參數(shù)或返回值,請使用 ExecuteNonQuery。這避免了用于創(chuàng)建空 DataReader 的任何不必要處理。
有關(guān)更多信息,請參閱 Executing a Command。
測試 Null
如果表(在數(shù)據(jù)庫中)中的列允許為空,就不能測試參數(shù)值是否“等于”空。相反,需要寫一個 WHERE 子句,測試列和參數(shù)是否都為空。下面的 SQL 語句返回一些行,它們的 LastName 列等于賦給 @LastName 參數(shù)的值,或者 LastName 列和 @LastName 參數(shù)都為空。
SELECT * FROM CustomersWHERE ((LastName = @LastName) OR (LastName IS NULL AND @LastName IS NULL))
把 Null 作為參數(shù)值傳遞
對數(shù)據(jù)庫的命令中,當(dāng)把空值作為參數(shù)值發(fā)送時,不能使用 null(Visual Basic廬 .NET 中為 Nothing)。而需要使用 DBNull.Value。例如:
‘Visual BasicDim 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;
執(zhí)行事務(wù)
ADO.NET 的事務(wù)模型已經(jīng)更改。在 ADO 中,當(dāng)調(diào)用 StartTransaction 時,調(diào)用之后的任何更新操作都被視為是事務(wù)的一部分。但是,在 ADO.NET 中,當(dāng)調(diào)用 Connection.BeginTransaction 時,會返回一個 Transaction 對象,需要把它與 Command 的 Transaction 屬性聯(lián)系起來。這種設(shè)計可以在一個單一連接上執(zhí)行多個根事務(wù)。如果未將 Command.Transaction 屬性設(shè)置為一個針對相關(guān)的 Connection 而啟動的 Transaction,那么 Command 就會失敗并引發(fā)異常。
即將發(fā)布的 .NET 框架將使您可以在現(xiàn)有的分布式事務(wù)中手動登記。這對于對象池方案來說很理想;在該方案中,一個池對象打開一次連接,但是在多個獨立的事務(wù)中都涉及到該對象。.NET 框架 1.0 發(fā)行版中這一功能并不可用。
有關(guān)事務(wù)的更多信息,請參閱 Performing Transactions 以及 .NET Data Access Architecture Guide。
高性能應(yīng)用程序與使用中的數(shù)據(jù)源保持最短時間的連接,并且利用性能增強技術(shù),例如連接池。下面的主題提供一些技巧,有助于在使用 ADO.NET 連接到數(shù)據(jù)源時獲得更好的性能。
連接池
用于 ODBC 的 SQL Server、OLE DB 和 .NET 框架數(shù)據(jù)提供程序隱式緩沖連接。通過在連接字符串中指定不同的屬性值,可以控制連接池的行為。有關(guān)如何控制連接池的行為的詳細(xì)信息,請參閱 Connection Pooling for the SQL Server .NET Data Provider 和 Connection Pooling for the OLE DB .NET Data Provider。
用 DataAdapter 優(yōu)化連接
DataAdapter 的 Fill 和 Update 方法在連接關(guān)閉的情況下自動打開為相關(guān)命令屬性指定的連接。如果 Fill 或 Update 方法打開了連接,Fill 或 Update 將在操作完成的時候關(guān)閉它。為了獲得最佳性能,僅在需要時將與數(shù)據(jù)庫的連接保持為打開。同時,減少打開和關(guān)閉多操作連接的次數(shù)。
如果只執(zhí)行單個的 Fill 或 Update 方法調(diào)用,建議允許 Fill 或 Update 方法隱式打開和關(guān)閉連接。如果對 Fill 和/或 Update 調(diào)用有很多,建議顯式打開連接,調(diào)用 Fill 和/或 Update,然后顯式關(guān)閉連接。
另外,當(dāng)執(zhí)行事務(wù)時,顯式地在開始事務(wù)之前打開連接,并在提交之后關(guān)閉連接。例如:
‘Visual BasicPublic 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();}
始終關(guān)閉 Connection 和 DataReader
完成對 Connection 或 DataReader 對象的使用后,總是顯式地關(guān)閉它們。盡管垃圾回收最終會清除對象并因此釋放連接和其他托管資源,但垃圾回收僅在需要時執(zhí)行。因此,確保任何寶貴的資源被顯式釋放仍然是您的責(zé)任。并且,沒有顯式關(guān)閉的 Connections 可能不會返回到池中。例如,一個超出作用范圍卻沒有顯式關(guān)閉的連接,只有當(dāng)池大小達到最大并且連接仍然有效時,才會被返回到連接池中。
注 不要在類的 Finalize 方法中對 Connection、DataReader 或任何其他托管對象調(diào)用 Close 或 Dispose。最后完成的時候,僅釋放類自己直接擁有的非托管資源。如果類沒有任何非托管資源,就不要在類定義中包含 Finalize 方法。
在 C# 中使用 "Using" 語句
對于 C# 程序員來說,確保始終關(guān)閉 Connection 和 DataReader 對象的一個方便的方法就是使用 using 語句。using 語句在離開自己的作用范圍時,會自動調(diào)用被“使用”的對象的 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 屬性
如果連接已經(jīng)打開,OleDbConnection.State 屬性會對 DBPROP_CONNECTIONSTATUS 屬性的 DATASOURCEINFO 屬性集執(zhí)行本地 OLE DB 調(diào)用 IDBProperties.GetProperties,這可能會導(dǎo)致對數(shù)據(jù)源的往返行程。也就是說,檢查 State 屬性的代價可能很高。所以僅在需要時檢查 State 屬性。如果需要經(jīng)常檢查該屬性,監(jiān)聽 OleDbConnection 的 StateChange 事件可能會使應(yīng)用程序的性能好一些。有關(guān) StateChange 事件的詳細(xì)信息,請參閱 Working with Connection Events。
ADO.NET 在 DataSet 中提供了廣泛的 XML 集成,并公開了 SQL Server 2000 及其更高版本提供的部分 XML 功能。還可以使用 SQLXML 3.0 廣泛地訪問 SQL Server 2000 及其更高版本中的 XML 功能。下面是使用 XML 和 ADO.NET 的技巧和信息。
DataSet 和 XML
DataSet 與 XML 緊密集成,并提供如下功能:
• | 從 XSD 架構(gòu)中加載 DataSet 的架構(gòu)或關(guān)系型結(jié)構(gòu)。 |
• | 從 XML 加載 DataSet 的內(nèi)容。 |
• | 如果沒有提供架構(gòu),可以從 XML 文檔的內(nèi)容推斷出 DataSet 的架構(gòu)。 |
• | 把 DataSet 的架構(gòu)寫成 XSD 架構(gòu)。 |
• | 把 DataSet 的內(nèi)容寫成 XML。 |
• | 同步訪問使用 DataSet 的數(shù)據(jù)的關(guān)系表示,以及使用 XmlDataDocument 的數(shù)據(jù)的層次表示。 |
注 可以使用這種同步把 XML 功能(例如,XPath 查詢和 XSLT 轉(zhuǎn)換)應(yīng)用到 DataSet 中的數(shù)據(jù),或者在保留原始 XML 保真度的前提下為 XML 文檔中數(shù)據(jù)的全部或其中一個子集提供關(guān)系視圖。
關(guān)于 DataSet 提供的 XML 功能的詳細(xì)信息,請參閱 XML and the DataSet。
架構(gòu)推斷
從 XML 文件加載 DataSet 時,可以從 XSD 架構(gòu)加載 DataSet 架構(gòu),或者在加載數(shù)據(jù)前預(yù)定義表和列。如果沒有可用的 XSD 架構(gòu),而且不知道為 XML 文件的內(nèi)容定義哪些表和列,就可以在 XML 文檔結(jié)構(gòu)的基礎(chǔ)上對架構(gòu)進行推斷。
架構(gòu)推斷作為遷移工具很有用,但應(yīng)只限于設(shè)計階段應(yīng)用程序,這是由于推斷處理有如下限制。
• | 對架構(gòu)的推斷會引入影響應(yīng)用程序性能的附加處理。 |
• | 所有推斷列的類型都是字符串。 |
• | 推斷處理不具有確定性。也就是說,它是基于 XML 文件內(nèi)容的,而不是預(yù)定的架構(gòu)。因此,對于兩個預(yù)定架構(gòu)相同的 XML 文件,由于它們的內(nèi)容不同,結(jié)果得到兩個完全不同的推斷架構(gòu)。 |
有關(guān)更多信息,請參閱 Inferring DataSet Relational Structure from XML。
用于 XML 查詢的 SQL Server
如果正從 SQL Server 2000 FOR XML 返回查詢結(jié)果,可以讓用于 SQL Server 的 .NET 框架數(shù)據(jù)提供程序使用 SqlCommand.ExecuteXmlReader 方法直接創(chuàng)建一個 XmlReader。
SQLXML 托管類
.NET 框架中有一些類,公開用于 SQL Server 2000 的 XML 的功能。這些類可在 Microsoft.Data.SqlXml 命名空間中找到,它們添加了執(zhí)行 XPath 查詢和 XML 模板文件以及把 XSLT 轉(zhuǎn)換應(yīng)用到數(shù)據(jù)的能力。
SQLXML 托管類包含在用于 Microsoft SQL Server 2000 的 XML (SQLXML 2.0) 發(fā)行版中,可從 XML for Microsoft SQL Server 2000 Web Release 2 (SQLXML 2.0) ??μ?。
下面是一些編寫 ADO.NET 代碼時的通用技巧。
避免自動增量值沖突
就像大多數(shù)數(shù)據(jù)源一樣,DataSet 使您可標(biāo)識那些添加新行時自動對其值進行遞增的列。在 DataSet 中使用自動增量的列時,如果自動增量的列來自數(shù)據(jù)源,可避免添加到 DataSet 的行和添加到數(shù)據(jù)源的行之間本地編號沖突。
例如,考慮一個表,它的主鍵列 CustomerID 是自動增量的。兩個新的客戶信息行添加到表中,并接收到自動增量的 CustomerID 值 1 和 2。然后,只有第二個客戶行被傳遞給 DataAdapter 的方法 Update,新添加的行在數(shù)據(jù)源接收到一個自動增量的 CustomerID 值 1,與 DataSet 中的值 2 不匹配。當(dāng) DataAdapter 用返回值填充表中第二行時,就會出現(xiàn)約束沖突,因為第一個客戶行已經(jīng)使用了 CustomerID 值 1。
要避免這種情況,建議在使用數(shù)據(jù)源上自動增量的列以及 DataSet 上自動增量的列時,把 DataSet 中的列創(chuàng)建為 AutoIncrementStep 值等于 -1 并且 AutoIncrementSeed 值等于 0,另外,還要確保數(shù)據(jù)源生成的自動增量標(biāo)識值從 1 開始,并且以正階值遞增。因此,DataSet 為自動增量值生成負(fù)數(shù),與數(shù)據(jù)源生成的正自動增量值不沖突。另外一個選擇是使用 Guid 類型的列,而不是自動增量的列。生成 Guid 值的算法應(yīng)該永遠不會使數(shù)據(jù)源中生成的 Guid 值與 DataSet 中生成的 Guid 值一樣。
如果自動增量的列只是用作唯一值,而且沒有任何意義,就考慮使用 Guid 代替自動增量的列。它們是唯一的,并且避免了使用自動增量的列所必需的額外工作。
有關(guān)從數(shù)據(jù)源檢索自動增量的列值的示例,請參閱 Retrieving Identity or AutoNumber Values。
檢查開放式并發(fā)沖突
按照設(shè)計,由于 DataSet 是與數(shù)據(jù)源斷開的,所以,當(dāng)多個客戶端在數(shù)據(jù)源上按照開放式并發(fā)模型更新數(shù)據(jù)時,需要確保應(yīng)用程序避免沖突。
在測試開放式并發(fā)沖突時有幾項技術(shù)。一項技術(shù)涉及在表中包含時間戳列。另外一項技術(shù)是,驗證一行中所有列的原始值是否仍然與通過在 SQL 語句中使用 WHERE 子句進行測試時在數(shù)據(jù)庫中找到的值相匹配。
有關(guān)包含代碼示例的該主題的詳細(xì)討論,請參閱 Optimistic Concurrency。
多線程編程
ADO.NET 對性能、吞吐量和可伸縮性進行優(yōu)化。因此,ADO.NET 對象不鎖定資源,并且必須只用于單線程。一個例外是 DataSet,它對多個閱讀器是線程安全的。但是,在寫的時候需要把 DataSet 鎖定。
僅在需要的時候才用 COM Interop 訪問 ADO
ADO.NET 的設(shè)計目的是成為許多應(yīng)用程序的最佳解決方案。但是,有些應(yīng)用程序需要只有使用 ADO 對象才有的功能,例如,ADO 多維 (ADOMD)。在這些情況下,應(yīng)用程序可以用 COM Interop 訪問 ADO。注意使用 COM Interop 訪問具有 ADO 的數(shù)據(jù)會導(dǎo)致性能降低。在設(shè)計應(yīng)用程序時,首先在實現(xiàn)用 COM Interop 訪問 ADO 的設(shè)計之前,先確定 ADO.NET 是否滿足設(shè)計需求。
摘自【李天平】