ADO.NET提供了一个统一的编程模式和一组公用的类来进行任何类型的数据访问,而不管你用何种语言来开发代码。ADO.NET是全新的,但又与ADO尽可能保持一致,它使编程模式从一个客户端/服务器、基于连接的模式转变到了一个新的模式,这个新模式可以让断开的前端下载记录、离线工作、然后重新连接来提交变化。ADO.NET是WinForms应用程序、ASP.NET应用程序和Web services的一个共有的特点。其功能可以跨LAN和Internet连接来实现,可以在有状态(stateful)和无状态(stateless)情况下实现。
这就意味着,作为一个共有的技术,ADO.NET的对象在所有可能的环境中并不是同等强大的。用ADO.NET为一个富客户端(rich client)构建一个数据层同为一个客户端通常是共享的和重要的实体(如Web服务器)的Web应用程序构建一个数据层并不一样。
如果你从前是个ADO开发人员,现在已经用ADO.NET了,那么你可能把数据访问看做是一个万能的对象,如Recordset。我们很自然地会将旧的对象模式同新的对象模式匹配起来,并将现有的方法用于.NET应用程序。然而,在ADO环境中的某些好的方法在转换到ADO.NET环境时就可能并不强大了。而且,看起来很微不足道的ADO.NET对象模式的复杂性可能会导致很糟糕的编程情况、不理想的代码、甚至是功能不能实现。我将讲述在ADO.NET编程中可能会给你带来麻烦的10个方面,并提供技巧和解决方法来避免它们。
1. 避免Database-Agnostic形式的编程
ADO.NET中的数据访问是强类型的,就是说在任何时候你都必须了解你正在处理的是什么数据源(data source)。相反,在ADO中,你可以编写数据访问代码(它们充分利用了OLE DB提供者的通用模式),并将基本的数据源只看做是个参数。ADO对象模式提供了唯一的连接和命令对象,它们隐藏了基本的DBMS的特征。一旦你在Connection对象上设置了Provider属性,那么为SQL Server或Oracle创建一个命令对象就需要同样的代码。许多开发人员都通过该功能来使用生产环境外的Access数据库,以便很快地测试或演示应用程序。
在ADO.NET中是不能这么做的,因为在ADO.NET中,至少连接对象必须是特定于数据源的。你不能以一种间接或通用的方式来创建连接,除非你决定运用ADO的数据访问技术——OLE DB。在ADO.NET中,你可以用OleDbConnection类创建到一个数据库的连接,这个类可以让你访问各种数据源。在.NET托管环境中运用System.Data.OleDb名字空间中的类并不特别有效,因为它们是用OLE DB来访问数据的。你只能用OLE DB来访问那些没有.NET数据提供者的数据源。
如果你的应用程序必须访问全异的数据源(而且你知道可能涉及什么数据源——一个合理的假设),那么你可以创建一个集中的factory类,它返回一个连接对象,并通过一个通用的接口(IDbConnection接口)来管理这个连接对象。Factory类在内部运用应用程序参数来决定使用什么.NET数据提供者:' Create the connection
Dim factory As New MyAppConnectionFactory
Dim conn As IDbConnection
conn = factory.CreateConnection(connString)
' Create the command
Dim cmd As IDbCommand = conn.CreateCommand(query)
一旦你得到了一个连接对象,你就可以以database-agnostic的方式来创建和执行一个命令了,而不管使用的数据源是什么。你可以使用CreateCommand方法并通过IDbCommand接口来引用命令。然后,你可以用IDbCommand接口上的ExecuteReader方法或ExecuteNonQuery方法来执行命令。如果你用ExecuteReader,你就可以得到一个data reader并可以用IDataReader接口来对它进行一般的访问了。
你不能用一个通用的数据库编程模式来填充一个DataSet对象。实际上,你不能像创建一个命令那样以一种间接的方式来创建data adapter对象。原因就是,在有些情况下,data adapter不同于命令对象,它可以在内部隐含地创建一个连接。然而,它必须以一种强类型的方式工作,而且必须知道基本的数据库服务器是什么。
2. 运用字符串来串行化扩展的属性
几个ADO.NET对象都拥有一个叫做ExtendedProperties的集合。该属性就像收集货物(cargo collection)一样,可以用来存储任何类型的用户信息。DataSet、DataTable和DataColumn就是可以提供该数据成员的类。ADO.NET通过运用PropertyCollection类封装的一个哈希表来实现这个ExtendedProperties属性。你可以用Add方法将数据插入到集合中。Add方法使用了两个参数来保存数据——key和value。该方法的原形将参数定义为通用的对象类型,你可以存储任何类型的信息。然而,在特殊情况下,你应该特别注意那些被保存为扩展属性的对象的类型。
如果你想将包含扩展属性的ADO.NET对象串行化到XML,最好只用字符串。如果不行,你必须对ADO.NET的内在的serializer的行为采取对策。
当ADO.NET将一个DataSet对象保存到XML时,ExtendedProperties集合的内容就被串行化到内存中了,但大概是出于性能的原因,ADO.NET运用了ToString方法,而不是XML serializer来实现串行化。更重要的是,当ADO.NET对象被读回并复原时,ExtendedProperties集合包含的是对象的字符串表现形式,而不是对象本身。
3. 运用具有BLOB字段的ExecuteXmlReader
用于SQL Server的.NET数据提供者(data provider)使用了数据库提供的XML扩展名,并提供了一个额外的方法(ExecuteXmlReader)来执行查询。命令对象上的所有的执行者(例如ExecuteReader和ExecuteScaler)都采用不同的方法来得到结果集。ExcecuteReader通过一个托管指针(managed cursor)(data reader)来返回数据,而ExecuteScaler返回结果集中的第一个值,把它作为一个标量值。ExecuteXmlReader执行查询,并返回已经绑定到一个XmlTextReader对象的基于XML的输出流。通过这种方式,你就不需要做额外的工作来以XML的方式加工数据了。要实现这一点,查询字符串必须返回XML数据。对SQL Server来说,当查询字符串包含一个FOR XML子句时,就可以实现它。尽管这只是一种可能。
一个不太为人所知的情况是,要使ExecuteXmlReader工作,让结果集包含XML数据就足够了。 下面的查询方法很好,只要列包含XML格式的文本就行:SELECT data FROM table WHERE key=1
这个列是个典型的BLOB或ntext字段,其文本显示为XML。简要地看看ExecuteXmlReader方法的内部结构会有助于我们的理解。该方法用ExecuteReader来执行查询,并从数据提供者得到一个数据流对象。接下来,它将数据流绑定到XmlTextReader类的一个新创建的实例上,这个实例被返回给调用者。连接一直处于忙碌状态,直到XML reader停止工作。SQL Server提供者是唯一的提供者,它提供了方法让我们从一个XML reader直接读取数据,但这种做法更多的是与提供者有关,而与数据库性能的关系并不大。Oracle支持XML查询,但Oracle的数据提供者并不支持XML查询。相比之下,为OLE DB数据提供者编写一个ExecuteXmlReader方法并不难(点此下载实例)。
4. 不要设法缓存一个DataView
DataSet和DataTable对象是唯一的包含数据的ADO.NET对象。DataView是一个不能串行化的、轻量级的类,它只代表构建在一个表上的视图(view)。你可以根据一个表达式或行的状态来过滤视图。许多应用程序都需要你管理数据视图并将它们绑定到数据控件上,如Windows和Web DataGrid控件。一个DataView对象不能缓存数据;它只是缓存了与当前过滤器相匹配的基本的表中的行的索引。缓存索引的顺序与当前的排序表达式一致。缓存DataView而不缓存基本的DataTable是不行的。
例如,提供分页(比如通过运用DataGrid控件)的ASP.NET应用程序通常以一个DataView对象结尾,因为它支持排序和过滤。在有些情况下(大多是基于性能的原因),你可能决定要缓存数据源。要缓存的对象不能是DataView(它是你实际绑定的对象)。一个DataView只是一种索引,如果没有基本的DataTable对象,它是没有用的。
5. 运用Find来读取一个记录
通过运用DataTable的Select方法来运行一个内存中的查询,或在视图上设置一个过滤器来滤掉与指定标准不匹配的所有的记录,你就可以读取一个DataTable对象中的一个特定的行了。你可以通过设置DataView类上的RowFilter属性来设置一个过滤器。这两种方法都运用相同的引擎来选择记录。它们可以接纳一个表达式,对它进行解析并求各个子句的值。DataTable的Select方法返回一个带有所有相匹配的DataRow对象的数组。RowFilter属性重建DataView的内部索引来包含所有的(且仅包含)匹配的记录。然后,应用程序就可以访问记录了。这两种方法在性能上几乎是一样的;运用哪种方法取决于环境和个人喜好。例如,如果你用的是数据绑定的控件,如一个DataGrid或DataList,那么RowFilter就很理想。如果你必须处理一串记录,那么Select方法就更好了。
然而,你还可以用另一种方法(仍然是基于DataView的),它是读取一个表中的记录的最快的方法。该方法就是用Find:
Dim view As DataView
view = New DataView(table)
view.Sort = "orderid"
Dim index As Integer = view.Find(10248)
Dim row As DataRow = view(index).Row
Find方法运用了视图的当前索引,并将指定的值(或多个值)与形成当前索引的字段匹配起来。在前面的代码中,值10248与列orderid匹配。如果Sort属性为空,且DataTable对象有一个主键,那么就运用主键中的列。Find方法返回的是相匹配的第一行的基于0的位置的值。
如果你想返回多个记录,可以用FindRows的演变形式: view.Sort = "orderid, discount"
Dim keys(1) As Object
keys(0) = 10248
keys(1) = 0
Dim row As DataRow = _
view(view.Find(keys)).Row
前面的代码可以让你通过运用Find的重载方法(带有一组对象)来匹配多个列的值。
6. 尽可能用预先排序的数据
ADO.NET对象模式使我们可以很容易地实现排序。你可以创建一个DataView对象并设置其Sort属性;ADO.NET runtime查看新的排序表达式并为视图重编索引。该步骤是在内存中实现的,但速度并不快。排序的花费很高,更重要的是,它并不是个线性操作(linear operation)。对一组数据进行排序需要n*log(n)的计算成本,就是说,随着需要排序的条目数量的增加,直线增加的成本是很大的。因此,你应该限制应用程序中的排序,尽可能地运用预先排序的数据。在Web应用程序中,动态排序对性能的影响是相当大的。既然如此,你就应该设计应用程序,限制对动态排序的需求,并依赖在数据库服务器中写死的算法。除非你在用应用程序的一个可以使复杂性低于n*log(n)极限的特殊的功能,否则避免运用手工排序算法,因为这种算法可能比系统中的算法更糟。
7. ADOX可以帮你得到并改变Schema信息
ADO.NET并没有为得到并管理schema信息提供一个完全的对象模式。你应该用ActiveX Data Objects Extensions for Data Definition Language and Security (ADOX)或用每个数据库提供的本地功能来得到并改变Schema信息。ADOX是ADO对象的一个扩展,它包括用来创建和修改Schema的对象。你可以编写适用于各种数据源的代码(不管本地语法有什么不同),因为ADOX是管理schema的一个基于对象的方法。
你可以用一个data reader对象来读(不是设置)简单的schema信息。所有的data reader类(OleDbDataReader、SqlDataReader、OracleDataReader)都提供了一个GetSchemaTable方法,该方法可以读取查询到的列的元数据信息。GetSchemaTable返回一个DataTable对象(格式是每列一行)和固定的一组包含信息的列。返回的元数据可以分成三类:列元数据、数据库特征和列属性。返回的列可以是AllowDBNull、IsAutoIncrement、ColumnName、IsExpression、IsReadonly和NumericPrecision等。在MSDN资料中有完整的列表(见附加资源)。
在调用ExecuteReader时,如果你执行KeyInfo命令,那么GetSchemaTable方法就可以返回更精确的数据。你可以将KeyInfo行为同缺省的行为结合起来,执行一个单独的命令并得到schema和数据:reader = cmd.ExecuteReader( _ CommandBehavior.KeyInfo Or _ CommandBehavior.CloseConnection)
只有执行KeyInfo,IsKey、BaseTableName、IsAliased、IsExpression和IsHidden字段的值才能被正确返回。如果执行KeyInfo,关键的列(如果有)通常是添加在结果集的底部的,但不给它们返回数据。
8. 用一个派生的类和自定义的串行化来节省空间
只有两个ADO.NET对象是被标记为可串行化的——DataTable和DataSet。.NET Framework中的串行化是通过formatter对象来完成的,它们可以将一个对象实例保存到一个二进制或一个SOAP流(stream)中。.NET formatter用Reflection来提取任何必要的信息。然而,如果这个类实现了ISerializable接口,那么.NET formatter就会给接口的方法让步,让它们负责拷贝需要串行化到一个内存缓冲器中的所有的信息。DataTable和DataSet类都通过ISerializable接口支持串行化。
如果你将一个DataTable或一个DataSet串行到一个二进制(binary stream)中,你应该可以得到非常紧凑的输出结果。虽然你得到的结果文件是最小的,但遗憾的是,它实际上并不小。荒谬的是,你保存到一个二进制的DataSet比你用WriteXml方法保存到XML的同样的DataSet要大很多。
要解释这种情况,我们需要来看看ADO.NET对象是用什么方式被串行起来的。在串行一个DataSet对象时,它将基于XML的DiffGram表示法保存在formatter的缓冲器中。在串行一个DataTable时,它首先创建了一个临时的DataSet对象,将它定义为它的parent,然后作为一个DiffGram串行起来。
一个DiffGram是一个XML流,它提供了一个DataSet中表和行的有状态的表示法。一个DiffGram文件是很详细的,有些冗长。DiffGram包含当前的数据,以及被修改的行和未解决的错误的初始值。当我们保存一个DataSet或一个DataTable时,所有这些信息就会被传递给serializer。被串行化的对象总是包含XML数据,因此即使当输出流是二进制的时,最后的输出结果仍然会很大。
你可以创建一个继承DataTable或DataSet的新的可串行化的类来解决这个问题,并且更有效地保存ADO.NET对象。你必须用<Serizlizable()>属性来标记新类,即使父类是可以串行化的。实际上,串行性(serizlizability)并不是一个可以自动继承的类属性。你从DataTable或DataSet构建的新类也可以实现ISerializable接口。当然,你可以为新类选择一个不同的串行化方案。一个简单而有效的方法就是将DataTable类的所有成员映射到数组和值成员中。
运用一个派生的类和一个自定义的串行化方案可以为一个DataSet对象节省多达80%的磁盘空间。节省的空间的比率取决于DataSet中的数据类型。你的数据越基于文本,节省的空间越多。然而,运用二进制的BLOB字段只可以节省大约25%的空间(下载一个完整的例子)。
9. 选择一个适合你的数据的分页机制
DataGrid服务器控件使我们可以更容易地在Web应用程序中以长度可变的页面来显示数据了。该控件有绑定和格式化功能,它可以接受一个ADO.NET数据对象并为浏览器生成HTML代码。出于性能的原因,在页面的视图状态,DataGrid并没有缓存数据源的内容。因此,当返回页面时,你就必须填充grid。要实现这一点可以用两种方法:在Web服务器上将数据源作为整体或一部分缓存起来,然后读回;或者对每个请求从物理数据库加载所需的记录。如果你选择第一种方法,那么数据就从存储中只被读取一次,保存在一个缓存中,并为以后的postback事件读回。我们通常用内存中的全局对象(如Session或Cache)来保存这个数据。我们用DataSet来搜集所有需要的数据并将它保存在内存中。将一个DataSet对象保存在Session中同ADO中的线程含义并不一样,但是通过减少Web服务器可用的内存仍可以影响可扩展性。
如果要显示的数据是特定于session的,那么在每次返回页面时加载记录页面就比用一个DataSet和ASP.NET全局对象来缓存数据要好。编写得很好的SQL代码可以将结果集分成许多页,再加上DataGrid控件内置的自定义分页机制,我们就可以得到最佳的解决方案来保持ASP.NET应用程序的可扩展性和良好的性能了。
对于Windows应用程序,我的建议正好相反。台式应用程序很适合应用断开的编程模式(DataSet和其它ADO.NET对象使这种模式变得更简单了)。当然,这并不意味着,你可以在客户端无忧无虑地下载成千上万的记录。尽管你可以将ADO.NET对象用于任何种类的.NET应用程序,但如何使用它们是随具体情况的不同而不同的。
10. 访问多个结果集
根据查询的语法,你可以返回多个结果集。缺省情况下,data reader是位于第一个结果集上的。你可以用Read方法在当前结果集中滚动查看记录。在找到最后一个记录时,Read方法返回false,不再继续读取。你应该用NextResult方法转移到下一个结果集。如果没有更多的需要读的结果集了,那么该方法返回false。下面的代码说明了如何在所有返回的结果集中访问所有的记录:Dim reader As SqlDataReader
cmd.Connection.Open()
reader = cmd.ExecuteReader()
Do
' Move through the first resultset
While reader.Read()
' access the row
End While
Loop While reader.NextResult()
reader.Close()
cmd.Connection.Close()
当你读一个行的内容时,可以通过索引或名称来识别列。运用索引可以更快,因为提供者可以直接进入到缓冲器中。如果你指定列名,提供者就用GetOrdinal方法将名称转换成相应的索引,然后执行基于索引的访问。注意,对于SQL Server data reader来说,所有的GetXXX方法实际上都调用了相应的GetSqlXXX方法。对于Oracle data reader来说,情况是类似的,本地数据总是被写进.NET Framework类型中。OracleDataReader类为它自己的内部类型提供了一组私有的GetXXX方法。这些方法包括GetOracleBFile、GetOracleBinary和GetOracleDateTime等。相反,OLE DB和ODBC readers只有单独的一组get方法。
.NET Framework 1.1版通过添加方法HasRows扩展了data readers的编程接口,该方法返回一个Boolean值来说明是否有很多行需要读。(这是ASP.NET 1.0的一个不足之处。)然而,该方法并没有告诉我们有效的行的数量。同样,也没有方法或技巧使我们提前知道已经返回了多少结果集。
在Oracle数据库编程中,一个查询或一个存储过程返回的多个结果集是通过多个REF CURSOR对象处理的。有多少结果集,你就必须将多少输出参数同命令关联起来,以便NextResult方法可以用于Oracle数据库。在命令文本中,一个ADO.NET结果集同一个Oracle REF CURSOR是一致的。输出参数名必须与指针名匹配,它们的类型必须是OracleType.Cursor。例如,如果要运行的存储过程(或命令文本)引用了两个指针(Employees和Orders),那么下面的代码就说明了如何进行设置以返回两个结果集:Dim p1 As OracleParameter
p1 = cmd.Parameters.Add("Employees", OracleType.Cursor)
p1.Direction = ParameterDirection.Output
Dim p2 As OracleParameter
p2 = cmd.Parameters.Add("Orders", OracleType.Cursor)
p2.Direction = ParameterDirection.Output
在上面的代码中,cmd是一个OracleCommand对象,它指向一个命令或一个存储过程。它执行代码,创建了两个REF CURSOR,称为Employees和Orders。REF CURSOR的名称和ADO.NET输出参数的名称必须匹配。
ADO.NET对象模式包含两个主要的部分——托管提供者和database-agnostic的容器类,如DataSet。托管提供者是数据源连接器的新类型;它们代替了基于COM的OLE DB提供者。到我写这篇文章时为止,只有少数几个托管提供者来连接商业DBMS。.NET Framework 1.1版只包含几个本地提供者——用于SQL Server、Oracle和所有OLE DB的提供者和ODBC驱动程序。第三方的供应商也支持MySQL并为Oracle提供了可供选择的提供者。
ADO.NET看起来类似于ADO,而且托管提供者在结构上同OLE DB提供者也是可以相比的。除了这些相似点外,在ADO.NET中进行有效的编程还需要一套新的技巧和好的方法。在大多数情况下,你可以通过编写代码得到很多技巧,并积累对象模式方面的经验。当你在进一步研究ADO.NET编程时,记住我在本文中所讲的这10个ADO.NET技巧吧。