ADONET作为微软最新的数据访问技术已经在企业开发中得到了广泛的应用对于一线的开发人员来说掌握基本的概念和技术之后提高应用水平和解决实际问题的最有效手段莫过于相互交流彼此的最佳时间经验经验在这篇文章中两位ADONET专家向读者毫无保留地详尽地介绍了很多实用经验
简介
本文为您提供了在Microsoft ADONET应用程序中实现和获得最佳性能可伸缩性以及功能的最佳解决方案同时也讲述了使用ADONET中可用对象的最佳实践并提出一些有助于优化ADONET应用程序设计的建议
NET框架数据提供程序
NET框架中的数据提供程序(Data Provider)在应用程序和数据源之间起到桥梁作用……NET框架数据提供程序能够从数据源中返回查询结果对数据源执行命令将DataSet中的更改传播给数据源本文包括有关哪个NET框架数据提供程序是最适合您需要的一些技巧
使用哪个NET框架数据提供程序?
为了使您的应用程序获得最佳性能请使用最适合您的数据源的NET框架数据提供程序有许多数据提供程序可供您的应用程序选用
连接到SQL Server 或更高版本
为了在连接到Microsoft SQL Server 或更高版本时获得最佳性能请使用SQL Server NET数据提供程序SQL Server NET数据提供程序的设计目的就在于不通过任何附加技术层就可以直接访问SQL Server
连接到ODBC数据源
ODBC NET数据提供程序可在MicrosoftDataODBC命名空间中找到它的体系结构与用于SQL Server和OLE DB的NET数据提供程序相同ODBC NET数据提供程序遵循命名约定以ODBC为前缀(例如OdbcConnection)并使用标准ODBC连接字符串
使用DataReaderDataSetDataAdapter和DataView
ADONET提供以下两个对象用于检索关系数据并将其存储在内存中DataSet和DataReaderDataSet提供一个内存中数据的关系表示形式一整套包括一些表在内的数据(这些表包含数据对数据进行排序并约束数据)以及表之间的关系DataReader提供一个来自数据库的快速仅向前只读数据流
当使用DataSet时经常会利用DataAdapter(也可能是CommandBuilder)与数据源进行交互当使用DataSet时也可以利用DataView对DataSet中的数据应用排序和筛选也可以从DataSet继承创建强类型DataSet用于将表行和列作为强类型对象属性公开
下列主题包括的信息涉及使用DataSet或DataReader的最佳时机如何优化访问它们所包含数据以及如何优化使用DataAdapter(包括CommandBuilder)和DataView的技巧
DataSet与DataReader
当设计应用程序时要考虑应用程序所需功能的等级以确定使用DataSet或者是DataReader
要通过应用程序执行以下操作就要使用DataSet
) 在结果的多个离散表之间进行导航
) 操作来自多个数据源(例如来自多个数据库一个XML文件和一个电子表格的混合数据)的数据
) 在各层之间交换数据或使用XML Web服务与DataReader不同的是DataSet能传递给远程客户端
) 重用同样的记录集合以便通过缓存获得性能改善(例如排序搜索或筛选数据)
) 每条记录都需要执行大量处理对使用DataReader返回的每一行进行扩展处理会延长服务于DataReader的连接的必要时间这影响了性能
) 使用XML操作对数据进行操作例如可扩展样式表语言转换(XSLT转换)或XPath查询
对于下列情况要在应用程序中使用DataReader
) 不需要缓存数据
) 要处理的结果集太大内存中放不下
) 一旦需要以仅向前只读方式快速访问数据
注填充DataSet时DataAdapter使用DataReader因此使用DataAdapter取代DataSet提升的性能表现为节省了DataSet占用内存和填充DataSet需要的循环一般来说此性能提升只是象征性的因此设计决策应以所需功能为基础
使用强类型DataSet的好处
DataSet的另一个好处是可被继承以创建一个强类型DataSet强类型DataSet的好处包括设计时类型检查以及Microsoft Visual StudioNET用于强类型DataSet语句结束所带来的好处修改了DataSet的架构或关系结构后就可以创建一个强类型DataSet将行和列作为对象的属性公开而不是作为集合中的项公开例如不公开客户表中行的姓名列而公开Customer对象的Name属性类型化DataSet从DataSet类派生因此不会牺牲DataSet的任何功能也就是说类型化DataSet仍能远程访问并作为数据绑定控件(例如DataGrid)的数据源提供如果架构事先不可知仍能受益于通用DataSet的功能但却不能受益于强类型DataSet的附加功能
处理强类型DataSet中的空引用
使用强类型DataSet时可以使用DataSet的XML架构定义语言(XSD)架构来确保强类型DataSet可以正确处理空引用nullValue标识符使您可用一个指定的值StringEmpty代替DBNull保留空引用或引发异常选择哪个选项取决于应用程序的上下文默认情况下如果遇到空引用就会引发异常
刷新DataSet中的数据
如果想用服务器上的更新值刷新DataSet中的值就使用DataAdapterFill如果有在DataTable上定义的主键DataAdapterFill会根据主键进行新行匹配并且当更改到现有行时应用服务器上的值即使刷新之前修改了这些数据刷新行的RowState仍被设置为Unchanged注意如果没有为DataTable定义主键DataAdapterFill就用可能重复的主键值添加新行
如果想用来自服务器的当前值刷新表并同时保留对表中的行所做的任何更改必须首先用DataAdapterFill填充表并填充一个新的DataTable然后用preserveChanges值true将DataTable合并到DataSet之中
在DataSet中搜索数据
在DataSet中查询与特定条件相匹配的行时可以利用基于索引的查找提高搜索性能当将PrimaryKey值赋给DataTable时会创建一个索引当给DataTable创建DataView时也会创建一个索引下面是一些利用基于索引进行查找的技巧
) 如果对组成DataTable的PrimaryKey的列进行查询要使用DataTableRowsFind而不是DataTableSelect
) 对于涉及到非主键列的查询可以使用DataView为数据的多个查询提高性能当将排序顺序应用到DataView时就会建立一个搜索时使用的索引DataView公开Find和FindRows方法以便查询基础DataTable中的数据
) 如果不需要表的排序视图仍可以通过为DataTable创建DataView来利用基于索引的查找注意只有对数据执行多个查询操作时这样才会带来好处如果只执行单一查询创建索引所需要的处理就会降低使用索引所带来的性能提升
DataView构造
如果创建了DataView并且修改了SortRowFilter或RowStateFilter属性DataView就会为基础DataTable中的数据建立索引创建DataView对象时要使用DataView构造函数它用SortRowFilter和RowStateFilter值作为构造函数参数(与基础DataTable一起)结果是创建了一次索引创建一个空DataView并随后设置SortRowFilter或RowStateFilter属性会导致索引至少创建两次
分页
ADONET可以显式控制从数据源中返回什么样的数据以及在DataSet中本地缓存多少数据对查询结果的分页没有唯一的答案但下面有一些设计应用程序时应该考虑的技巧
) 避免使用带有startRecord和maxRecords值的DataAdapterFill重载当以这种方式填充DataSet时只有maxRecords参数(从startRecord参数标识的记录开始)指定的记录数量用于填充DataSet但无论如何总是返回完整的查询这就会引起不必要的处理用于读取不需要的记录而且为了返回附加记录会耗尽不必要的服务器资源
) 用于每次只返回一页记录的技术是创建SQL语句将WHERE子句以及ORDER BY子句和TOP谓词组合起来此技术取决于存在一种可唯一标识每一行的办法当浏览下一页记录时修改WHERE子句使之包含所有唯一标识符大于当前页最后一个唯一标识符的记录当浏览上一页记录时修改WHERE子句使之返回所有唯一标识符小于当前页第一个唯一标识符的记录两种查询都只返回记录的TOP页当浏览上一页时需要以降序为结果排序这将有效地返回查询的最后一页(如果需要显示之前也许要重新排序结果)
) 另一项每次只返回一页记录的技术是创建SQL语句将TOP谓词和嵌入式SELECT语句的使用结合在一起此技术并不依赖于存在一种可唯一标识每一行的办法使用这项技术的第一步是将所需页的数量与页大小相乘然后将结果传递给SQL Query的TOP谓词该查询以升序排列再将此查询嵌入到另一个查询中后者从降序排列的嵌入式查询结果中选择TOP页大小实质上返回的是嵌入式查询的最后一页例如要返回查询结果的第三页(页大小是)应该书写如下所示的命令
SELECT TOP * FROM (SELECT TOP * FROM Customers ORDER BY Id ASC) AS Table ORDER BY Id DESC
注意从查询中返回的结果页以降序显示如果需要应该重新排序
) 如果数据不经常变动可以在DataSet中本地维护一个记录缓存以此提高性能例如可以在本地DataSet中存储页有用的数据并且只有当用户浏览超出缓存第一页和最后一页时才从数据源中查询新数据
用架构填充DataSet
当用数据填充DataSet时DataAdapterFill方法使用DataSet的现有架构并使用从SelectCommand返回的数据填充它如果在DataSet中没有表名与要被填充的表名相匹配Fill方法就会创建一个表默认情况下Fill仅定义列和列类型
通过设置DataAdapter的MissingSchemaAction属性可以重写Fill的默认行为例如要让Fill创建一个表架构并且还包括主键信息唯一约束列属性是否允许为空最大列长度只读列和自动增量的列就要将DataAdapterMissingSchemaAction指定为MissingSchemaActionAddWithKey或者在调用DataAdapterFill前可以调用DataAdapterFillSchema来确保当填充DataSet时架构已到位
对FillSchema的调用会产生一个到服务器的额外行程用于检索附加架构信息为了获得最佳性能需要在调用Fill之前指定DataSet的架构或者设置DataAdapter的MissingSchemaAction
使用CommandBuilder的最佳实践
假设SelectCommand执行单一表SELECTCommandBuilder就会以DataAdapter的SelectCommand属性为基础自动生成DataAdapter的InsertCommandUpdateCommand和DeleteCommand属性下面是为获得最佳性能而使用CommandBuilder的一些技巧
) CommandBuilder的使用应该限制在设计时或即席方案中生成DataAdapter命令属性所必需的处理会影响性能如果预先知道INSERT/UPDATE/DELETE语句的内容就显式设置它们一个比较好的设计技巧是为INSERT/UPDATE/DELETE命令创建存储过程并显式配置DataAdapter命令属性以使用它们
) CommandBuilder使用DataAdapter的SelectCommand属性确定其他命令属性的值如果DataAdapter的SelectCommand本身曾经更改过确保调用RefreshSchema以更新命令属性
) 如果DataAdapter命令属性为空(命令属性默认情况下为空)CommandBuilder仅仅为它生成一条命令如果显式设置了命令属性CommandBuilder不会重写它如果希望CommandBuilder为以前已经设置过的命令属性生成命令就将命令属性设置为空
批处理SQL语句
很多数据库支持将多条命令合并或批处理成一条单一命令执行例如SQL Server使您可以用分号分隔命令将多条命令合并成单一命令能减少到服务器的行程数并提高应用程序的性能例如可以将所有预定的删除在应用程序中本地存储起来然后再发出一条批处理命令调用从数据源删除它们
虽然这样做确实能提高性能但是当对DataSet中的数据更新进行管理时可能会增加应用程序的复杂性要保持简单可能要在DataSet中为每个DataTable创建一个DataAdapter
用多个表填充DataSet
如果使用批处理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() daFill(ds Customers) //C# SqlDataAdapter da = new SqlDataAdapter(SELECT * FROM Customers SELECT * FROM Orders myConnection) DataSet ds = new DataSet() daFill(ds Customers)
来自Customers表的数据放在名为Customers的DataTable中来自Orders表的数据放在名为Customers的DataTable中
填充完DataSet之后可以很容易地将Customers表的TableName属性改为Orders但是后面的填充会导致Customers表被重新填充而Orders表会被忽略并创建另外一个Customers表为了对这种情况作出补救创建一个DataTableMapping将Customers映射到Orders并为其他后面的表创建其他的表映射例如
Visual Basic Dim da As SqlDataAdapter = New SqlDataAdapter(SELECT * FROM Customers SELECT * FROM Orders myConnection) daTableMappingsAdd(Customers Orders) Dim ds As DataSet = New DataSet() daFill(ds Customers) //C# SqlDataAdapter da = new SqlDataAdapter(SELECT * FROM Customers SELECT * FROM Orders myConnection) daTableMappingsAdd(Customers Orders) DataSet ds = new DataSet() daFill(ds Customers)
使用DataReader
下面是一些使用DataReader获得最佳性能的技巧同时还回答了一些关于使用DataReader的常见问题
) 在访问相关Command的任何输出参数之前必须关闭DataReader
) 完成读数据之后总是要关闭DataReader如果使用Connection只是用于返回DataReader那么关闭DataReader之后立刻关闭它
另外一个显式关闭Connection的方法是将CommandBehaviorCloseConnection传递给ExecuteReader方法以确保相关的连接在关闭DataReader时被关闭如果从一个方法返回DataReader而且不能控制DataReader或相关连接的关闭则这样做特别有用
) 不能在层之间远程访问DataReaderDataReader是为已连接好的数据访问设计的
) 当访问列数据时使用类型化访问器例如GetStringGetInt等这使您不用进行将GetValue返回的Object强制转换成特定类型所需的处理
) 一个单一连接每次只能打开一个DataReader在ADO中如果打开一个单一连接并且请求两个使用只进只读游标的记录集那么ADO会在游标生存期内隐式打开第二个未池化的到数据存储区的连接然后再隐式关闭该连接对于ADONET秘密完成的动作很少如果想在相同的数据存储区上同时打开两个DataReaders就必须显式创建两个连接每个DataReader一个这是ADONET为池化连接的使用提供更多控制的一种方法
) 默认情况下DataReader每次Read时都要将整行加载到内存这允许在当前行内随机访问列如果不需要这种随机访问为了提高性能就将CommandBehaviorSequentialAccess传递给ExecuteReader调用这将DataReader的默认行为更改为仅在请求时将数据加载到内存注意CommandBehaviorSequentialAccess要求顺序访问返回的列也就是说一旦读过返回的列就不能再读它的值了
) 如果已经完成读取来自DataReader的数据但仍然有大量挂起的未读结果就在调用DataReader的Close之前先调用Command的Cancel调用DataReader的Close会导致在关闭游标之前检索挂起的结果并清空流调用Command的Cancel会放弃服务器上的结果这样DataReader在关闭的时候就不必读这些结果如果要从Command返回输出参数还要调用Cancel放弃它们如果需要读取任何输出参数不要调用Command的Cancel只要调用DataReader的Close即可
二进制大对象(BLOB)
用DataReader检索二进制大对象(BLOB)时应该将CommandBehaviorSequentialAccess传递给ExecuteReader方法调用因为DataReader的默认行为是每次Read都将整行加载到内存又因为BLOB值可能非常大所以结果可能由于单个BLOB而使大量内存被用光SequentialAccess将DataReader的行为设置为只加载请求的数据然后还可以使用GetBytes或GetChars控制每次加载多少数据
记住使用SequentialAccess时不能不按顺序访问DataReader返回的不同字段也就是说如果查询返回三列其中第三列是BLOB并且想访问前两列中的数据就必须在访问BLOB数据之前先访问第一列的值然后访问第二列的值这是因为现在数据是顺序返回的并且DataReader一旦读过该数据该数据就不再可用