ADO.NET 的最佳实践技巧


这是我很早以前看过的微软的一篇文章,最近,一些网友问的问题很多理论都在里面,所以,整理一下放在这里,大家可以参考一下。

简介

本文为您提供了在 Microsoft ADO.NET 应用程序中实现和获得最佳性能、可伸缩性以及功能的最佳解决方案;同时也讲述了使用 ADO.NET 中可用对象的最佳实践;并提出一些有助于优化 ADO.NET 应用程序设计的建议。

本文包含:

有关 .NET 框架包含的 .NET 框架数据提供程序的信息。

DataSetDataReader 之间的比较,以及这些对象中每个对象最佳用法的解释。

解释如何使用 DataSetCommandsConnections

有关与 XML 集成的信息。

通用的技巧和问题。

Working with a Typed DataSet。

刷新 DataSet 中的数据

如果想用服务器上的更新值刷新 DataSet 中的值,就使用 DataAdapter.Fill如果有在 DataTable 上定义的主键,DataAdapter.Fill 会根据主键进行新行匹配,并且当更改到现有行时应用服务器上的值。即使刷新之前修改了它们,刷新行的 RowState 仍被设置为 Unchanged。注意,如果没有为 DataTable 定义主键,DataAdapter.Fill 就用可能重复的主键值添加新行。

如果想用来自服务器的当前值刷新表,并同时保留对表中的行所做的任何更改,必须首先用 DataAdapter.Fill 填充表,并填充一个新的 DataTable,然后用 preserveChangestrueDataTableMergeDataSet 中。

在 DataSet 中搜索数据

DataSet 中查询与特定条件相匹配的行时,可以利用基于索引的查找提高搜索性能。当把 PrimaryKey 值赋给 DataTable 时,会创建一个索引。当给 DataTable 创建 DataView 时,也会创建一个索引。下面是一些利用基于索引进行查找的技巧。

如果对组成 DataTablePrimaryKey的列进行查询,要使用 DataTable.Rows.Find 而不是 DataTable.Select

对于涉及到非主键列的查询,可以使用 DataView 为数据的多个查询提高性能。当把排序顺序应用到 DataView 时,就会建立一个搜索时使用的索引。DataView 公开 FindFindRows 方法,以便查询基础 DataTable 中的数据。

如果不需要表的排序视图,仍可以通过为 DataTable 创建 DataView 来利用基于索引的查找。注意,只有对数据执行多个查询操作时,这样才会带来好处。如果只执行单一查询,创建索引所需要的处理就会降低使用索引所带来的性能提升。

DataView 构造

如果创建了 DataView,并且修改了 SortRowFilterRowStateFilter 属性,DataView 就会为基础 DataTable 中的数据建立索引。创建 DataView 对象时,要使用 DataView 构造函数,它用 SortRowFilterRowStateFilter 值作为构造函数参数(与基础 DataTable 一起)。结果是创建了一次索引。创建一个“空”DataView 并随后设置 SortRowFilterRowStateFilter 属性,会导致索引至少创建两次。

分页

ADO.NET 可以显式控制从数据源中返回什么样的数据,以及在 DataSet 中本地缓存多少数据。对查询结果的分页没有唯一的答案,但下面有一些设计应用程序时应该考虑的技巧。

避免使用带有 startRecordmaxRecords 值的 DataAdapter.Fill 重载。当以这种方式填充 DataSet 时,只有 maxRecords 参数(从 startRecord 参数标识的记录开始)指定的记录数量用于填充 DataSet,但无论如何总是返回完整的查询。这就会引起不必要的处理,用于读取“不需要的”记录;而且为了返回附加记录,会耗尽不必要的服务器资源。

用于每次只返回一页记录的技术是创建 SQL 语句,把 WHERE 子句以及 ORDER BY 子句和 TOP 谓词组合起来。此技术取决于存在一种可唯一标识每一行的办法。当浏览下一页记录时,修改 WHERE 子句使之包含所有唯一标识符大于当前页最后一个唯一标识符的记录。当浏览上一页记录时,修改 WHERE 子句使之返回所有唯一标识符小于当前页第一个唯一标识符的记录。两种查询都只返回记录的 TOP 页。当浏览上一页时,需要以降序为结果排序。这将有效地返回查询的最后一页(如果需要,显示之前也许要重新排序结果)。有关这个技术的一个示例,请参阅 Paging Through a Query Result。

另一项每次只返回一页记录的技术是创建 SQL 语句,把 TOP 谓词和嵌入式 SELECT 语句的使用结合在一起。此技术并不依赖于存在一种可唯一标识每一行的办法。使用这项技术的第一步是把所需页的数量与页大小相乘。然后将结果传递给 SQL Query 的 TOP 谓词,该查询以升序排列。再把此查询嵌入到另一个查询中,后者从降序排列的嵌入式查询结果中选择 TOP 页大小。实质上,返回的是嵌入式查询的最后一页。例如,要返回查询结果的第三页(页大小是 10),应该书写如下所示的命令:

SELECT TOP 10 * FROM
      (SELECT TOP 30 * FROM Customers ORDER BY Id ASC) AS Table1
      ORDER BY Id DESC

注意,从查询中返回的结果页以降序显示。如果需要,应该重新排序。

如果数据不经常变动,可以在 DataSet 中本地维护一个记录缓存,以此提高性能。例如,可以在本地 DataSet 中存储 10 页有用的数据,并且只有当用户浏览超出缓存第一页和最后一页时,才从数据源中查询新数据。

有关更多信息,请参阅 .NET Data Access Architecture Guide。

用架构填充 DataSet

当用数据填充 DataSet 时,DataAdapter.Fill 方法使用 DataSet 的现有架构,并使用从 SelectCommand 返回的数据填充它。如果在 DataSet 中没有表名与要被填充的表名相匹配,Fill 方法就会创建一个表。默认情况下,Fill 仅定义列和列类型。

通过设置 DataAdapterMissingSchemaAction 属性,可以重写 Fill 的默认行为。例如,要让 Fill 创建一个表架构,并且还包括主键信息、唯一约束、列属性、是否允许为空、最大列长度、只读列和自动增量的列,就要把 DataAdapter.MissingSchemaAction 指定为 MissingSchemaAction.AddWithKey。或者,在调用 DataAdapter.Fill 前,可以调用 DataAdapter.FillSchema 来确保当填充 DataSet 时架构已到位。

FillSchema 的调用会产生一个到服务器的额外行程,用于检索附加架构信息。为了获得最佳性能,需要在调用 Fill 之前指定 DataSet 的架构,或者设置 DataAdapterMissingSchemaAction

使用 CommandBuilder 的最佳实践

假设 SelectCommand 执行单一表 SELECT,CommandBuilder 就会以 DataAdapterSelectCommand 属性为基础自动生成 DataAdapterInsertCommandUpdateCommand、和 DeleteCommand 属性。下面是为获得最佳性能而使用 CommandBuilder 的一些技巧。

CommandBuilder 的使用应该限制在设计时或即席方案中。生成 DataAdapter 命令属性所必需的处理会影响性能。如果预先知道 INSERT/UPDATE/DELETE 语句的内容,就显式设置它们。一个比较好的设计技巧是,为 INSERT/UPDATE/DELETE 命令创建存储过程并显式配置 DataAdapter 命令属性以使用它们。

CommandBuilder 使用 DataAdapterSelectCommand 属性确定其他命令属性的值。如果 DataAdapterSelectCommand 本身曾经更改过,确保调用 RefreshSchema 以更新命令属性。

如果 DataAdapter 命令属性为空(命令属性默认情况下为空),CommandBuilder 仅仅为它生成一条命令。如果显式设置了命令属性,CommandBuilder 不会重写它。如果希望 CommandBuilder 为以前已经设置过的命令属性生成命令,就把命令属性设置为空。

Obtaining BLOB Values from a Database。

返回页首
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 作为参数值传递

对数据库的命令中,当把空值作为参数值发送时,不能使用 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 对象,需要把它与 CommandTransaction 属性联系起来。这种设计可以在一个单一连接上执行多个根事务。如果未将 Command.Transaction 属性设置为一个针对相关的 Connection 而启动的 Transaction,那么 Command 就会失败并引发异常。

即将发布的 .NET 框架将使您可以在现有的分布式事务中手动登记。这对于对象池方案来说很理想;在该方案中,一个池对象打开一次连接,但是在多个独立的事务中都涉及到该对象。.NET 框架 1.0 发行版中这一功能并不可用。

有关事务的更多信息,请参阅 Performing Transactions 以及 .NET Data Access Architecture Guide。

返回页首
Connection Pooling for the SQL Server .NET Data Provider 和 Connection Pooling for the OLE DB .NET Data Provider。

用 DataAdapter 优化连接

DataAdapterFillUpdate 方法在连接关闭的情况下自动打开为相关命令属性指定的连接。如果 FillUpdate 方法打开了连接,FillUpdate 将在操作完成的时候关闭它。为了获得最佳性能,仅在需要时将与数据库的连接保持为打开。同时,减少打开和关闭多操作连接的次数

如果只执行单个的 FillUpdate 方法调用,建议允许 FillUpdate 方法隐式打开和关闭连接。如果对 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();
}

始终关闭 Connection 和 DataReader

完成对 ConnectionDataReader 对象的使用后,总是显式地关闭它们。尽管垃圾回收最终会清除对象并因此释放连接和其他托管资源,但垃圾回收仅在需要时执行。因此,确保任何宝贵的资源被显式释放仍然是您的责任。并且,没有显式关闭的 Connections 可能不会返回到池中。例如,一个超出作用范围却没有显式关闭的连接,只有当池大小达到最大并且连接仍然有效时,才会被返回到连接池中

不要在类的 Finalize 方法中对 ConnectionDataReader 或任何其他托管对象调用 CloseDispose。最后完成的时候,仅释放类自己直接拥有的非托管资源。如果类没有任何非托管资源,就不要在类定义中包含 Finalize 方法

在 C# 中使用 "Using" 语句

对于 C# 程序员来说,确保始终关闭 ConnectionDataReader 对象的一个方便的方法就是使用 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 属性会对 DBPROP_CONNECTIONSTATUS 属性的 DATASOURCEINFO 属性集执行本地 OLE DB 调用 IDBProperties.GetProperties,这可能会导致对数据源的往返行程。也就是说,检查 State 属性的代价可能很高。所以仅在需要时检查 State 属性。如果需要经常检查该属性,监听 OleDbConnectionStateChange 事件可能会使应用程序的性能好一些。有关 StateChange 事件的详细信息,请参阅 Working with Connection Events。

返回页首
XML and the DataSet。

架构推断

从 XML 文件加载 DataSet 时,可以从 XSD 架构加载 DataSet 架构,或者在加载数据前预定义表和列。如果没有可用的 XSD 架构,而且不知道为 XML 文件的内容定义哪些表和列,就可以在 XML 文档结构的基础上对架构进行推断。

架构推断作为迁移工具很有用,但应只限于设计阶段应用程序,这是由于推断处理有如下限制。

对架构的推断会引入影响应用程序性能的附加处理。

所有推断列的类型都是字符串。

推断处理不具有确定性。也就是说,它是基于 XML 文件内容的,而不是预定的架构。因此,对于两个预定架构相同的 XML 文件,由于它们的内容不同,结果得到两个完全不同的推断架构。

有关更多信息,请参阅 Inferring DataSet Relational Structure from XML。

用于 XML 查询的 SQL Server

如果正从 SQL Server 2000 FOR XML 返回查询结果,可以让用于 SQL Server 的 .NET 框架数据提供程序使用 SqlCommand.ExecuteXmlReader 方法直接创建一个 XmlReader

SQLXML 托管类

.NET 框架中有一些类,公开用于 SQL Server 2000 的 XML 的功能。这些类可在 Microsoft.Data.SqlXml 命名空间中找到,它们添加了执行 XPath 查询和 XML 模板文件以及把 XSLT 转换应用到数据的能力。

SQLXML 托管类包含在用于 Microsoft SQL Server 2000 的 XML (SQLXML 2.0) 发行版中,可从 XML for Microsoft SQL Server 2000 Web Release 2 (SQLXML 2.0) ??μ?。

返回页首
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 是否满足设计需求。


« 
» 

Copyright © 2016 phpStudy | 豫ICP备2021030365号-3