概述:本文提供了在多层.NET应用程序中实施基于ADO.NET的数据访问层的指导原则。其重点是一组通用数据访问任务和方案,并指导你选择最合适的途径和技术(68张打印页)。
目录
简介
如果你在为.NET应用程序设计数据访问层,那么就应该把 Microsoft ADO.NET用作数据访问模型。ADO.NET扩展丰富,并且支持结合松散的数据访问需求、多层Web应用程序及Web服务。通常,它利用许多扩展丰富的对象模型, ADO.NET提供了多种方法用于解决一个特定问题。
本文将指导你选择最合适的数据访问方法,其做法是详细列出大范围的通用数据访问方案,提供运用技巧,并且建议最优实践。本文还回答了其它经常问到的问题:何处最适合存放数据库链接字符串?应如何实现链接存储池?如何处理事务?如何实现分页以允许用户在许多记录中滚动?
注意本文的重点是ADO.NET的使用:利用SQL Server .NETData Provider--随ADO.NET一起提供的两个供应器之一--访问Microsoft SQL Server 2000。本文在合适的地方,将突出显示在你使用OLE DB .NET数据供应器访问其它OLE DB敏感数据源时需要注意的所有差别。
对于利用本文所讨论的指导原则和最优实践所开发的数据访问组件的具体实现,见(Data Access Application Block)数据访问应用程序块。注意,本实现的源代码是可以获得的,并且能直接用于你的.NET应用程序中。
谁应当阅读本文?
本文为希望构建.NET应用程序的应用程序设计师和企业开发人员提供了指导原则。如果你负责设计并开发多层.NET应用程序的数据层,那么请阅读本文。
你首先需要知道什么?
要利用本指南构建.NET应用程序,你必须有利用ActiveX数据对象(ADO)和/或 OLE DB开发数据访问代码的实际经验,及SQL Server经验。你也必须明白如何为.NET平台开发管理代码,并且也必须清楚ADO.NET数据访问模型引入的基本变化。有关.NET开发的更多信息,见http://msdn.microsoft.com/net 。
ADO.NET是.NET应用程序的数据访问模型。它能用于访问关系型数据库系统,如SQL Server 2000,及很多其它已经配备了OLE DB供应器的数据源。在某种程度上,ADO.NET代表了最新版本的ADO技术。然而,ADO.NET引入了一些重大变化和革新,它们专门用于结构松散的、本质非链接的Web应用程序。关于ADO 与 ADO.NET的比较,见MSDN中的“用于ADO程序员的ADO.NET”一文。
ADO.NET引入的一个重要变化是,用DataTable, DataSet, DataAdapter, 和 DataReader对象的组合代替了ADO Recordset对象。DataTable表示来自一个表的行集合,在这方面它与Recordset类似。DataSet表示DataTable对象的集合,及与其它表绑定在一起的关系和限制。实际上,DataSet是具有内置的扩展标记语言(XML)支持的内存中的关联结构。
DataSet的一个主要特点是,它对底层的数据源一无所知,而这些数据源可能用于对其进行填充。这是一个分离的用于表示数据集合的独立实体,并且它可通过多层应用程序的不同层由一个组件传递到另一组件。它也可作为XML 数据流被序列化,因而非常适合于不同类型平台间的数据传输。ADO.NET使用DataAdapter对象为发送到和来自DataSet及底层数据源的数据建立通道。DataAdapter对象还支持增强的批更新特性,以前这是Recorder的相关功能。
图1显示了完整的DataSet对象模型。
图1 DataSet 对象模型
.NET 数据供应器
ADO.NET 依靠.NET 数据供应器的服务。 它们提供了对底层数据源的访问,包括四个主要对象(Connection, Command, DataReader,及DataAdapter),目前,ADO.NET只发行了两个供应器:
目前测试版中的其它.NET数据供应器包括:
名称空间组织
与每个.NET数据供应器相关的类型(类,结构,枚举,等等)位于它们各自的名称空间中:
在各自关联的名称空间中,每个供应器都提供了Connection, Command, DataReader, 及 DataAdapter对象的实现。SqlClient实现都有前缀"Sql";而OleDb实现前面都有前缀"OleDb"。例如,Connection对象的 SqlClient实现是SqlConnection。而OleDb实现是OleDbConnection。类似的,DataAdapter对象的两种实现是SqlDataAdapter 和OleDbDataAdapter。
通用编程
如果你很有可能以不同的数据源为目标,并希望将代码从一种数据源移植到另一数据源,那么可以考虑对System.Data名称空间中的IDbConnection, IDbCommand, IDataReader,和IDbDataAdapter接口进行编程。Connection, Command, DataReader, 及 DataAdapter对象的所有实现都必须支持这些接口。
关于实现.NET数据供应器的更多信息,见http://msdn.microsoft.com/library/en-us/cpguide/html/cpconimplementingnetdataprovider.asp.
图2显示了数据访问堆栈及ADO.NET如何与其它数据访问技术,包括ADO和OLE DB,联系起来。该图还显示了ADO.NET模型中的两个管理供应器和主要对象。
图2 数据访问堆栈
关于ADO到ADO.NET的演化,见MSDN杂志2000年11月期的文章“ADO+简介:用于微软.NET框架的数据访问服务”。
存储过程与直接SQL的比较
在本文剩余部分的大部分代码片段中,都使用了SqlCommand对象调用存储过程去执行数据库操作。在一些例子中,你见不到SqlCommand对象,因为存储过程名直接传递给了SqlDataAdapter对象,但这仍将导致SqlCommand对象的创建。
使用存储过程而非SQL语句的原因是:
属性与构造函数的比较
可以通过构造函数参数或直接设置属性来为ADO.NET对象设置具体的属性值。例如,下面的代码片段在功能上是等同的。
// Use constructor arguments to configure command object SqlCommand cmd = new SqlCommand( "SELECT * FROM PRODUCTS", conn ); // The above line is functionally equivalent to the following // three lines which set properties explicitly sqlCommand cmd = new SqlCommand(); cmd.Connection = conn; cmd.CommandText = "SELECT * FROM PRODUCTS";
从性能角度来说,两种方法的差别可以忽略,因为设置或获得.NET对象的属性比对COM对象执行类似操作要有效得多。
所作出的选择只是个人爱好和编码风格而已。然而,明确地设置属性的确使代码易于理解(特别是当你不熟悉ADO.NET对象模型时),便于调试。
注意 过去,VB开发人员被建议避免使用"Dim x As New…"结构创建对象。在COM环境中,这些代码将导致COM对象创建过程的“短路”,产生一些奇妙的和不怎么奇妙的错误。然而,在.NET环境中,这已不再是一个问题。
数据库链接是一种危险的、昂贵的、有限的资源,特别是在多层Web应用程序中。你必须正确管理你的链接,因为你的方法将极大的影响应用程序的整体升级性。还有,必须仔细考虑在哪儿存放链接字符串。你需要一个可配置的、安全的位置。
在管理数据库链接和链接字符串时,你应当努力:
本节讨论链接池,并帮你选择合适的链接池战略。其它可选方法也是存在的。本节也将考虑如何管理、存储、控制数据库链接字符串。最后,本节还提供了两个编码方案,使用它们将有助于确保链接已可靠关闭,并返回到链接池中。
链接池
数据库链接池使应用程序能够重用池中的现有链接,而不是重复地建立对数据库的链接。这种技术将极大地增加应用程序的可扩展性,因为有限的数据库链接可以为很多的客户提供服务。此技术也将提高性能,因为能够避免用于建立新链接的巨大时间。
数据访问技术,如ODBC和OLE DB,提供了多种形式的链接池,它们可配置到不同级别上。这两种方式对数据库客户端应用程序来说都是透明的。OLE DB链接池经常被称为会话或资源池。
关于微软数据访问组件(MDAC)中池的一般讨论,见http://msdn.microsoft.com/library/en-us/dnmdac/html/pooling2.asp。
ADO.NET数据供应器提供了透明的链接池,每种链接池的确切机制对每种供应器来说是不同的。本节讨论的链接池是关于:
用SQL Server .NET 数据供应器池化
如果正在使用SQL Server .NET数据供应器,那么就可使用该供应器提供的链接池化支持特性。它是由供应器在管理代码内内置实现的对事务敏感的高效机制。每个过程都将创建池,并且直到过程结束,池才被取消。
你可以透明地使用此种链接池,但应当清楚池是如何被管理的,并要知道可以用哪些选项来调整链接池。
如何配置SQL Server .NET数据供应器链接池
可以使用一组名称-值对以链接字符串的形式配置链接池。例如,可以配置池是否有效(默认是有效的),池的最大、最小容量,用于打开链接的排队请示被阻断的时间。下面的示例字符串配置了池的最大和最小容量。
"Server=(local); Integrated Security=SSPI; Database=Northwind; Max Pool Size=75; Min Pool Size=5"
当链接打开,池被创建时,多个链接增加到池中以使链接数满足所配置的最小值。此后,链接就能增加到池中,直到配置的最大池计数。当达到最大计数时,打开新链接的请求将排队一段可配置的时间。
选择池容量
能建立最大极限对于管理几千用户同时发出请求的大型系统来说是非常重要的。你需要监视链接池及应用程序的性能,以确定系统的最优池容量。最优容量还要依赖于运行SQL Server的硬件。
在开发期间,也许需要减小默认的最大池容量(目前是100)以帮助查找链接泄漏。
如果设立了最小池容量,那么当池最初被填充以达到该值时,会导致一些性能损失,尽管最初链接的几个客户会从中受益。注意,创建新链接的过程被序列化了,这就意味着当池最初被填充时,服务器无法处理同时发生的请求。
关于监视链接池的更多信息,见本文监视链接池一节。关于链接池链接字符串关键字的完整列表,见http://msdn.microsoft.com/library/en-us/cpguide/html/cpconconnectionpoolingforsqlservernetdataprovider.asp。
更多信息
在使用SQL Server .NET数据供应器链接池时,必须清楚:
SqlConnection conn = new SqlConnection( "Integrated Security=SSPI;Database=Northwind"); conn.Open(); // Pool A is created SqlConmection conn = new SqlConnection( "Integrated Security=SSPI ; Database=Northwind"); conn.Open(); // Pool B is created (extra spaces in string)
用OLE DB .NET数据供应器池化
OLE DB .NET数据供应器利用OLE DB资源池化的底层服务将链接存储到池中。很多方法可用于配置资源池化:
为了避开与注册表相关的部署问题,应避免使用注册表配置OLE DB资源池化。
关于OLE DB 资源池化的更多细节,见MSDN中“OLE DB程序员参考”一书的第19章:OLE DB服务中的资源池化部分。
用池化对象管理链接池化
作为Windows DNA开发人员,建议你使OLE DB资源池化和/或ODBC链接池化失效,并把COM+对象池化用作将数据库链接存储到池中的技术。这样做主要出于两个原因:
然而,由于SQL Server .NET数据供应器内置地使用池化,所以(在使用此供应器时)你不再需要开发自己的对象池化机制。这样就可以避免手工事务征募带来的复杂性。
如果正在使用OLE DB .NET数据供应器,那么考虑COM+对象池化以从高级配置和改进的性能中受益。如果你为此目的开发一个池化对象,那么必须使用OLE DB资源池化和自动事务征募失效(例如,通过将“OLE DB Services=-4”包含进链接字符串中)。必须在池化对象的实现中处理事务征募。
监视链接池化
要监视应用程序对链接池化的应用情况,可以使用随SQL Server发行的Profiler工具,或随微软Windows 2000发行的性能监视器。
要利用SQL Server Profiler 监视链接池化,操作如下:
要通过性能监视器监视链接池化,操作如下:
注意 .NET框架的RTM版本将另外包含一组ADO .NET性能计数器(这些计数器能与性能监视器结合起来使用),这些计数器用于为SQL Server .NET数据供应器监视并积累链接池化状态。
管理安全性
尽管数据库链接池化提高了应用程序的整体扩展性,这也意味着你不再能够在数据库端管理安全性。这是因为为了支持链接池化,链接字符串必须是相同的。如果需要跟踪每个用户的数据库操作,那么考虑为每个操作增加一个参数,通过这个参数就可以传递用户身份,手工将用户活动记入数据库。
使用Windows 认证
在链接到SQL Server时,应当使用Windows认证,因为它提供了许多优点:
.NETBeta 2版的性能测试表明,使用Windows认证与使用SQL Server认证相比,要花费更多的时间才能打开池化的数据库链接。然而,尽管Windows认证的成本较高,但与执行一个命令或存储过程所花费的时间相比,其(引起的)性能损失相对来说并不重要。结果,上面所列出的Windows认证的优点通常会稍微超过性能损失。
同样,当打开一个池化链接时,在.NET框架的RTM版本中,Windows认证与SQL Server认证的差别有望变得更不明显。
避免在中间层中冒充
Windows认证需要访问数据库的Windows帐号。虽然看上去在中间层中使用冒充更符合逻辑,但必须避免这样做,因为损害链接池化并对应用程序的扩展性产生严重影响。
为了解决这个问题,考虑对有限的Windows帐号(而不是被认证的负责人)实施冒充,每个帐号代表一个特定的角色。
例如,可以考虑下面的方法:
注意 每个帐号必须是同一域或信任域中在Internet信息服务(IIS)和SQL Server中存在的域帐号;也可以是在每台计算机上创建(具有相同用户名和密码)的匹配帐号。
为网络库使用TCP/IP
SQL Server 7.0及其以后版本支持用于所有网络库的Windows认证。使用TCP/IP可以获得配置、性能及扩展性优点。关于使用TCP/IP的更多信息,见本文通过防火墙建立链接
存储链接字符串
有多种方法可存储链接字符串,每种方法具有不同程度的灵活性和安全性。尽管在源代码中对字符串进行硬编码提供了最优性能,但文件系统缓存确保了与在文凭系统外部存储字符串相关的性能损失可被忽略。实际上外部链接字符串(允许管理员进行配置)所提供的附加灵活性在任何情况下都是受欢迎的。
选择存储链接字符串的方法时,首先要考虑的两个重要因素是配置的安全性与简易性,其次是性能。
可以选择将数据库链接字符串存储在下列位置:
使用Windows认证访问SQL Server,就可以避免在链接字符串存储用户名和密码。如果 安全需求要求更严格的方式,那么就考虑以加密格式存储链接字符串。
对于ASP.NET Web应用程序,以加密格式将链接字符串存储在Web.config文件中是一种安全而可配置的解决方案。
注意,在链接字符串中将Persist Security Info命名值设置为假,就可以阻止利用SqlConnection 或OleDbConnection对象的ConnectionString属性返回对安全敏感的内容,如密码。
下面几个小节讨论了如何用这些方法存储链接字符串,并说明了相对的优点和缺点。这使你能根据特定的应用程序环境作出相应的的选择。
使用XML应用程序配置文件
可以使用元素appSettings将数据库链接字符串存储在应用程序配置文件的定制设置部分。该元素支持任意关键字-值对,如下面的代码片段所示:
<configuration> <appSettings> <add key="DBConnStr" value="server=(local);Integrated Security=SSPI;database=northwind"/> </appSettings> </configuration>
注意:appSettings元素现在在configuration元素下面,并且不能直接出现在system.web下面。
优点
缺点
安全性。尽管ASP.NET Internet 服务器应用程序编程接口(ISAPI)DLL阻止了客户直接访问带.config扩展名的文件,并且NTFS文件系统权限也用于进一步限制访问,但你可能仍希望避免以明文方式将这些内容存储在前端的Web服务器上。要增加安全性,需将链接字符串以加密格式存储在配置文件中。
更多信息
利用System.Configuration.ConfigurationSettings类的AppSettings静态属性,可以获取应用程序的定制设置。如下面的代码片段所示,此处假定先前示例的定置关键字为DBConnStr。
using System.Configuration; private string GetDBaseConnectionString() { return ConfigurationSettings.AppSettings["DBConnStr"]; }
关于配置.NET 框架应用程序的更多信息,见http://msdn.microsoft.com/library/en-us/cpguide/html/cpconconfiguringnetframeworkapplications.asp.
使用UDL文件
OLE DB .NET数据供应器支持在它的链接字符串中使用统一数据链接(UDL)文件名。可以以构建参数的形式将链接字符串传给OleDbConnection对象,或利用对象的ConnectionString属性设置链接字符串。
注意 SQL Server .NET数据供应器不支持在它的链接字符串中使用UDL文件。因此,只有使用OLE DB .NET数据供应器,此方法才有效。
对于OLE DB 供应器,要利用链接字符串引用UDL文件,使用“File Name=name.udl.”。
优点
标准方法。你也许已经在用UDL文件进行链接字符串的管理了。
缺点
更多信息
使用Windows注册表
可以利用定制关键字将链接字符串存储在Windows注册表中,但由于部署问题,建议不要使用。
优点
缺点
使用定置文件
可以使用定制文件来存储链接字符串,然而这种技术没有优点,因此并不推荐使用。
优点
缺点
使用构建参数和COM+目录
可以将链接字符串存储在COM+目录中,并利用对象的构造字符串将它自动地传递给对象。COM+在初始化对象,提供配置构造字符串后,将立即调用对象的Construct方法。
注意这个方法只用于服务组件。只有管理组件使用了其它服务,如分布式事务处理支持或对象池化时,才考虑使用此方法。
优点
缺点
更多信息
链接使用方式
不管何种.NET数据供应器,你必须总是:
为确保在方法返回前链接已经关闭,考虑使用下面两个代码片段中演示的方法。第一个示例使用了finally块,第二个示例使用了C# using声明,此声明确保了对象的Dispose方法被调用。
下面的代码确保finally块关闭了链接。注意,此方法只用于Visual Basic .NET及C#中,因为Visual Basic .NET支持结构化例外处理。
public void DoSomeWork() { SqlConnection conn = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand("CommandProc", conn ); cmd.CommandType = CommandType.StoredProcedure; try { conn.Open(); cmd.ExecuteNonQuery(); } catch (Exception e) { // Handle and log error } finally { conn.Close(); } }
现在的代码显示了另外一种方法,此方法使用了C# using声明。注意,Visual Basic .NET并不支持using声明,或任何功能相同的对应语句。
public void DoSomeWork() { // using guarantees that Dispose is called on conn, which will // close the connection. using (SqlConnection conn = new SqlConnection(connectionString)) { SqlCommand cmd = new SqlCommand("CommandProc", conn); fcmd.CommandType = CommandType.StoredProcedure; conn.Open(); cmd.ExecuteQuery(); } }
此方法也适用于其它对象,如SqlDataReader 或OleDbDataReader,在其它任何对象对当前链接进行处理前,这些对象必须被关闭。
ADO.NET错误生成后,将由.NET框架内置的底层结构化异常处理支持所处理。结果,在数据访问代码中的错误处理方式与应用程序中其它地方的错误处理方式完全相同。通过标准的.NET异常处理语法和技术,异常被检测到并被处理。
本节描述了如何开发强壮的数据访问代码,并解释了如何处理数据访问错误。本节还提供了与SQL Server .NET数据供应器相关的异常处理详尽指南。
.NET 异常
.NET数据供应器将特定的数据库的错误状态转化为标准的异常类型,应当在数据访问代码中对这些异常进行处理。通过相关的异常对象的属性,可以获得特定数据库的错误细节。
所有.NET异常类型最终是从System名称空间的Exception基类中派生的。.NET数据供应器释放特定的供应器异常类型。例如,一旦SQL Server 返回一个错误状态时,SQL Server .NET数据供应器释放SqlException对象。类似的,OLE DB .NET数据供应器释放 OleDbException类型的异常,此对象包含了由底层OLE DB供应器暴露的细节。
图3显示了.NET数据供应器异常的层次结构。注意,OleDbException类是从 ExternalException类派生的ExternalException类是所有COM例外的基类。对象的ErrorCode属性存储了OLE DB生成的COM HRESULT。
图3 NET数据供应器层次结构
缓存并处理.NET异常
要处理数据访问例外状态,将数据访问代码放在try块中,并在catch块中利用合适的过滤器捕获生成的任何例外。例如,当利用SQL Server .NET数据供应器编写数据访问代码时,应当捕获SqlException类型的异常,如下面的代码所示:
try { // Data access code } catch (SqlException sqlex) // more specific { } catch (Exception ex) // less specific { }
如果为不止一个catch声明提供了不同的过滤标准,记住,按最特殊类型到最不特殊类型的顺序排列它们。通过这种方式,catch块中最特殊类型将将为任何给定的类型所执行。
SqlException 类所暴露的属性包含了例外状态的细节。其中包括:
下面的代码片段演示了如何利用SQL Server .NET数据供应器处理SQL Server 错误状态:
using System.Data; using System.Data.SqlClient; using System.Diagnostics; // Method exposed by a Data Access Layer (DAL) Component public string GetProductName( int ProductID ) { SqlConnection conn = new SqlConnection( "server=(local);Integrated Security=SSPI;database=northwind"); // Enclose all data access code within a try block try { conn.Open(); SqlCommand cmd = new SqlCommand("LookupProductName", conn ); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add("@ProductID", ProductID ); SqlParameter paramPN = cmd.Parameters.Add("@ProductName", SqlDbType.VarChar, 40 ); paramPN.Direction = ParameterDirection.Output; cmd.ExecuteNonQuery(); // The finally code is executed before the method returns return paramPN.Value.ToString(); } catch (SqlException sqlex) { // Handle data access exception condition // Log specific exception details LogException(sqlex); // Wrap the current exception in a more relevant // outer exception and re-throw the new exception throw new DALException( "Unknown ProductID: " + ProductID.ToString(), sqlex ); } catch (Exception ex) { // Handle generic exception condition . . . throw ex; } finally { conn.Close(); // Ensures connection is closed } } // Helper routine that logs SqlException details to the // Application event log private void LogException( SqlException sqlex ) { EventLog el = new EventLog(); el.Source = "CustomAppLog"; string strMessage; strMessage = "Exception Number : " + sqlex.Number + "(" + sqlex.Message + ") has occurred"; el.WriteEntry( strMessage ); foreach (SqlError sqle in sqlex.Errors) { strMessage = "Message: " + sqle.Message + " Number: " + sqle.Number + " Procedure: " + sqle.Procedure + " Server: " + sqle.Server + " Source: " + sqle.Source + " State: " + sqle.State + " Severity: " + sqle.Class + " LineNumber: " + sqle.LineNumber; el.WriteEntry( strMessage ); } }
在SqlException catch块中,代码最初利用LogException帮助函数记录错误状态,此函数利用foreach声明枚举了Errors集合中特定于供应器的细节,并将错误细节记录到错误日志中。 Catch块中的代码然后将特定于SQL Server的例外封装在DALException类型的对象中,这样做对调用者的GetProductName方法更具有意义。例外处理程序使用关键字throw将例外传回调用者。
更多信息
从存储过程中生成错误
T-SQL提供了一个RAISERROR(注意拼写)函数。你可用此函数生成定置错误,并将错误返回客户。对于ADO.NET客户,SQL Server .NET数据供应器对这些数据错误进行解释,并把它们转化为SqlError对象。
使用RAISERROR函数是简单地方法是将消息文本作为第一个参数包括进来,然后指定严重及状态参数,如下面的代码片段所示:
RAISERROR( 'Unknown Product ID: %s', 16, 1, @ProductID )
在这个例子中,替代参数用于将当前产品ID作为错误消息文本的一部分返回,参数2是消息的严重性,参数3是消息状态。
更多信息
正确使用严重性等级
仔细选择错误严重性等级,并要清楚每个级别造成的冲击。错误严重性等级的范围是0-25,并且它用于指出SQL Server 2000所遇到的问题的类型。在客户端代码中,通过在SqlException类的Errors集合中检查SqlError对象的 Class属性,你可以获得错误的严重性。表1 指出了不同严重性等级的意义及所造成的冲击。
表1.错误严重性等级--冲击及意义
严重性等级 | 链接已关闭 | 生成SqlException对象 | 意义 |
10及其以下 | No | No | 通知型消息,并不表示犯错误状态。 |
11-16 | No | Yes | 可由用户修改的错误,例如,使用修改后的输入数据重试操作。 |
17-19 | No | Yes | 资源或系统错误。 |
20-25 | Yes | Yes | 致命的系统错误(包括硬件错误)。客户链接被终止。 |
控制自动化事务
SQL Server .NET数据供应器对它所遇到的任何严重性大于10的错误都抛出SqlException对象。当作为自动化(COM+)事务一部分的组件检测到SqlException对象后,该组件必须确保它能取消事务。这也许是,也许不是自动化过程,并要依赖该方法是否已经对AutoComplete属性作出了标记。
关于在自动化事务上下文中处理对象的更多信息,见本文中的确定事务结果一节。
得到通知型消息
10及其以下严重性等级用于表示通知型消息,并且不会引发SqlException对象的抛出。
要获得通知型消息:
public delegate void SqlInfoMessageEventHandler( object sender, SqlInfoMessageEventArgs e );
通过传递到你的事件处理处理程序中的SqlInfoMessageEventArgs对象,可以得到消息数据。此对象暴露了Errors属性,该属性包含一组SqlError对象--每个通知消息一个SqlError对象。下面的代码片段演示了如何注册用于记录通知型消息的事件处理程序。
public string GetProductName( int ProductID ) { SqlConnection conn = new SqlConnection( "server=(local);Integrated Security=SSPI;database=northwind"); try { // Register a message event handler conn.InfoMessage += new SqlInfoMessageEventHandler( MessageEventHandler ); conn.Open(); // Setup command object and execute it . . . } catch (SqlException sqlex) { // log and handle exception . . . } finally { conn.Close(); } } // message event handler void MessageEventHandler( object sender, SqlInfoMessageEventArgs e ) { foreach( SqlError sqle in e.Errors ) { // Log SqlError properties . . . } }
性能
本节介绍了一些常见的数据访问方案,对每种方案,以ADO.NET 数据访问代码的形式描述了最优性能和扩展性解决方案。在合适的地方,还对性能,功能及开发最作出了比较。本节考虑了下面的功能方案。
获取多行
在这个方案中,你要获取一组表格化数据,并在得到的行中重复执行某个操作。例如你得到了一组数据,并以非链接的方式处理,然后(可能通过Web服务)将它作为XML文档传递给客户应用程序。可选的,你也可以以HTML表的形式将这些数据显示出来。
为了帮助确定最合适的数据访问方法,考虑你是否需要(非链接)DataSet 对象的附加灵活性,还是只需要SqlDataReader对象提供的原有性能,这些性能非常适合于B2C Web应用程序的数据表示。图4显示了这两种基本场景。
注意用于填充DataSet的SqlDataAdapter利用SqlDataReader方法数据。
图4 多行数据访问方案
方法比较
当从数据源中获取多行时,你可以使用下面的方法:
SqlDataReader 与 DataSet/DataTable间的选择本质上是性能与功能间的选择。SqlDataReader 提供了最优性能,而DataSet提供了额外的功能与灵活性。
数据绑定
所有这三个对象都可以作为数据绑定控件的数据源。而DataSet 和 DataTable 可作为更广范围控件的数据源。这是因为DataSet 和 DataTable 实现了(生成Ilist接口)IlistSource接口,而SqlDataReader 实现了Ienumerable接口。许多能进行数据绑定的WinForm控件需要实现了Ilist接口的数据源。
这种不同是因为为每种对象类型设计的场景类型不同。DataSet (它包含 DataTable)是一个丰富的、非链接结构,它适合于Web和桌面(WinForm)应用程序。另一方面,数据阅读器已经为Web应用程序进行了优化,这种应用程序需要优化的、只能向前的数据访问。
检查将要绑定到的特定控件类型的数据源需求。
在应用程序层间传递数据
DataSet提供了可作为XML被任意操纵数据的关系图,并允许数据的非链接缓存拷贝在应用程序层与组件间传递。然而,SqlDataReader提供了更优化的性能,因为它避免了与创建DataSet相关的性能及内存开销。记住,DataSet对象的创建将导致多个子对象--包括DataTable, DataRow 和DataColumn--及作为这些子对象容器的集合对象的创建。
使用DataSet
使用SqlDataAdapter填充的DataSet对象,当:
更多信息
如果使用SqlDataAdapter生成DataSet 或 DataTable,需注意:
使用SqlDataReader
些劣情况,可以使用通过调用 SqlCommand 对象的ExecuteReader方法得到的SqlDataReader对象:
更多信息
如果使用SqlDataReader对象,请注意:
使用XmlReader
下列情况下,使用通过调用SqlCommand对象的ExecuteXmlReader方法得到的XmlReader对象:
更多信息
如果使用XmlReader,请注意:
获取单行数据
在这种场景中,将从数据源中获取包含一组指定列的单行数据。例如,你得到一个客户ID,并希望查找与客户相关的细节;或得到一个产品ID,并希望得到产品信息。
方法比较
如果要对从数据源中得到的一行数据执行绑定操作,可以用SqlDataAdapter对象填充DataSet 或DataTable对象,其方式与在先前讨论过的获取多行数据及重复场景中描述的方式相同。然而,除非特别需要DataSet 或DataTable对象的功能,否则应当避免创建这些对象。
如果需要获取单行数据,那么请使用下面的一种方法:
这两种方法都避免了在服务器端创建结果集,在客户端创建DataSet对象的不必要额外开销。每种方法的相对性能要依赖于强度等级及数据库链接池化是否被使能。当数据库链接池化使能时,性能测试表明存储过程方法在高强度环境下(同时存在200多链接)其性能比SqlDataReader方法高近30%。
使用存储过程输出参数
如下情况中使用存储过程输出参数:
更多信息
使用SqlDataReader对象
下列情况,需使用SqlDataReader对象:
更多信息
获取单项数据
在本场景中,要获取单项数据。例如,提供了产品ID后,希望查询单一的产品名;或,给出了客户名后,希望查询客户的信用等级。在这种场景中,为得到单项数据,通常不希望引发创建DataSet 对象或甚至是 DataTable对象的额外开销。
也许只希望检查数据库中是否存在特定的行。例如,当新用户在网站注册时,需要检查所选用户名是否已经存在。这是单项数据查询中很特殊的例子,但在此例子中,返回一个简单的布尔返回值就足够了。
方法比较
当从数据源获取单项数据时,考虑下面的方法:
ExecuteScalar方法直接返回数据项,因为它是为只返回单个值的查询设计的,与存储过程输出参数和SqlDataReader方法相比,它需要更少的代码。
从性能方面来说,应当使用存储过程输出或返回参数,因为测试结果表明,存储过程方法在从低强度到高强度环境中(从同时不到100浏览器链接到200浏览器链接)提供了一致的性能。
更多信息
需要经常配置互联网应用程序以使它能够通过防火墙链接到SQL Server。例如,许多Web应用程序及防火墙的主要结构组件是周边网络(也被称为DMZ或非军事化区),它们用于隔离高端Web服务器与内部网络。
通过防火墙链接到SQL Server时,需要对防火墙,客户和服务器进行明确配置。SQL Server提供了客户网络应用程序和服务器网络应用程序以帮助进行配置。
选择网络库
当通过防火墙建立链接时,使用SQL Server TCP/IP网络库来简化配置,这是SQL Server2000安装的默认选项。如果使用先前版本的SQL Server,那么分别利用客户端网络应用程序和服务器端网络应用程序检查TCP/IP是否在客户和服务器端已经被配置为默认的网络库。
除了配置优点,使用TCP/IP库还意味着:
必须在客户和服务器计算机上配置TCP/IP,因为大多数防火墙限制了流量通过的端口,所以必须仔细考虑SQL Server所使用的端口号。
配置服务器
SQL Server的默认实例监听1433端口。然而,SQL Server 2000的指定实例在它们首次开启时,动态地分配端口号。网络管理员有希望在防火墙打开一定范围的端口;因此,当随防火墙使用SQL Server的指定实例时,利用服务网络应用程序对实例进行配置,使它监听特定的端口。然后管理员对防火墙进行配置,以使防火墙允许流量到达特定的IP地址及服务器实例所监听的端口。
注意,客户端网络库所使用的源端口号在1024-5000间动态分配。这是TCP/IP客户端应用程序的标准作法,但这意味着防火墙必须允许途经此范围的任何端口流量能够通过。关于SQL Server所使用的端口的更多信息,在微软产品支持服务网站上,参见INF: P 通过防火墙对SQL Server进行通讯所需的TCP端口
动态查找指定实例
如果改变了SQL Server所监听的默认端口,那么就要对客户端进行配置,以使它链接到此端口。更多细节,见本文中的配置客户端
如果改变了SQL Server 2000默认实例的端口号,那么不修改客户端将导致链接错误。如果存在多个SQL Server 实例,最新版本的MDAC数据访问堆栈(2.6)将进行动态查找,并利用用户数据报协议(UDP)协商(通过UDP端口1434)对指定实例进行定位。尽管这种方法在开发环境下也许有效,但在现在环境中却不大可能正常工作,因为典型发问下防火墙阻止UDP协商流量的通过。
为了避开这种情况,总是将客户端配置为链接到已配置好的目的端口号。
配置客户端
应当对客户端进行配置以利用TCP/IP网络库链接到SQL Server,并且也应当确保客户端库使用了正确的目的端口号。
使用TCP/IP 网络库
利用SQL Server客户端网络库,可以对客户端进行配置。在某些安装版本中,可能没有将这个应用程序安装到客户端(如Web服务器)。在这种情况下,可以按如下方式之一解决:
注意 在使用SQL Server .NET数据供应器时,网络库的默认设置是使用“dbmssocn”。
指定端口
如果SQL Server的实例被配置为监听默认的1433以外的其它端口,那么通过以下操作,就能指定链接到的端口号:
注意 ServerName可以是IP地址,或域名系统(DNS)名,为了优化性能,可以使用IP 地址以避免DNS 查询。
分布式事务处理
如果开发了使用COM+分布式事务处理和微软分布式事务处理协调器(DTC)服务的服务组件,那么就需要对防火墙进行配置,以允许DTC流在不同DTC实例间及DTC与资源管理器(例如SQL Server)间流动。
有关为DTC开放端口的更多信息,见INFO:为通过防火墙工作,配置微软分布式事务处理协调器 (DTC)。
目前,很多应用程序除了处理许多传统的字符串和数字型数据外,还要处理象图形或声音--甚至复杂的数据格式,如视频格式的数据。图形、声音与视频的数据格式类型不一。然而从存储角度来说,它们都可被视为二进制数据块,通常将其称为BLOBs(二进制大对象)。
SQL Server提供了binary, varbinary, 和image数据格式来存储BLOBs。不考虑名称,BLOB数据也可被称为基于文件的数据。例如,你可能要存储与特定行相关的二进制长注释字段。SQL Server为此目的提供了ntext 和text数据类型。
通常,对于小于8KB的二进制数据,使用varbinary数据类型。对于超过此大小的二进制数据,使用image 。表2 汇集了每个数据类型的主要特性。
表2 数据类型特性
数据类型 | 大小 | 描述 |
binary | 范围从1-8KB。存储大小是指定大小加4字节。 | 固定长度的二进制数据 |
varbinary | 范围从1-8KB。存储大小是所提供数据的实际大小加4字节。 | 可变长度的二进制数据 |
image | 从0-2GB大小的可变长度二进制数据 | 大容量可变长度二进制数据 |
text | 从0-2GB大小的可变长度数据 | 字符型数据 |
ntext | 从0-2GB大小的可变长度数据 | 宽字节字符数据 |
何处存储BLOB数据
SQL Server 7.0及其以后版本已经提高了存储在数据库中的BLOB数据的使用性能。这种情况的一个原因是数据库页面大小已经增加到了8KB。结果,小于8KB的文本或图象数据不必再存储在页面单独的二进制树结构中,而是能被存储在单行中。这意味着读取和写入text, ntext, 或 image数据能象读取或写入字符或二进制字符串那样快。超出8KB后,将在行中建立一个指针,数据本身存储在独立数据页面的二进制树结构中,这不可避免会对性能产生冲击。
关于迫使text, ntext, 和 image数据存储在单行中的更多信息,见SQL Server在线图书中的使用text和image数据主题。
一个经常使用的处理BLOB数据的可选方法是,将BLOB数据存储在文件系统中,并在数据库列中存储一个指针(通常是一个统一资源定位器--URL链接)以引用正确的文件。对于SQL Server 7.0以前的版本,将BLOB数据存储在数据库外的文件系统中,可以提高性能。
然而,SQL Server 2000改进了BLOB支持,以及ADO.NET对读取和写入BLOB数据的支持,使在数据库中存储BLOB数据成为一种可行的方法。
在数据库中存储BLOB 数据的优点
将BLOB数据存储在数据库中,带来了很多优点:
将BLOB数据写入到数据库中
下面的代码演示了如何利用ADO.NET将从某个文件获得的二进制数据写入SQL Server image字段中。
public void StorePicture( string filename ) { // Read the file into a byte array FileStream fs = new FileStream( filename, FileMode.Open, FileAccess.Read ); byte[] imageData = new Byte[fs.Length]; fs.Read( imageData, 0, (int)fs.Length ); fs.Close(); SqlConnection conn = new SqlConnection(""); SqlCommand cmd = new SqlCommand("StorePicture", conn); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add("@filename", filename ); cmd.Parameters["@filename"].Direction = ParameterDirection.Input; cmd.Parameters.Add("@blobdata", SqlDbType.Image); cmd.Parameters["@blobdata"].Direction = ParameterDirection.Input; // Store the byte array within the image field cmd.Parameters["@blobdata"].Value = imageData; try { conn.Open(); cmd.ExecuteNonQuery(); } catch { throw; } finally { conn.Close(); } }
从数据库中读取BLOB数据
在通过ExecuteReader方法创建SqlDataReader对象以读取包含BLOB数据的行时,需使用CommandBehavior.SequentialAccess枚举值。如果没有此枚举值,阅读器一次只从服务器中向客户端发送一行数据。如果行包含了BOLB数据,这预示着要占用大量内存。通过利用枚举值,就获得了更好的控制权,因为BLOB数据只在被引用时才被发出(例如,利用GetBytes方法,可以控制读取的字节数)。这在下面的代码片段中进行了演示。
// Assume previously established command and connection // The command SELECTs the IMAGE column from the table conn.Open(); SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess); reader.Read(); // Get size of image data - pass null as the byte array parameter long bytesize = reader.GetBytes(0, 0, null, 0, 0); // Allocate byte array to hold image data byte[] imageData = new byte[bytesize]; long bytesread = 0; int curpos = 0; while (bytesread < bytesize) { // chunkSize is an arbitrary application defined value bytesread += reader.GetBytes(0, curpos, imageData, curpos, chunkSize); curpos += chunkSize; } // byte array 'imageData' now contains BLOB from database
注意使用CommandBehavior.SequentialAccess需要以严格的顺序访问列数据。例如,如果BLOB数据存在于第3列,并且还需要从第1,2列中读取数据,那么在读取第3列前必须先读取第1,2列。
实际上所有用于更新数据源的面向商业的应用程序都需要事务处理支持。通过提供四个基本担保,即众所周知的首字缩写ACID:可分性,一致性,分离性,和耐久性,事务处理将用于确保包含在一个或多个数据源中的系统的完整性。
例如,考虑一个基于Web的零售应用程序,它用于处理购买订单。每个订单需要3个完全不同操作,这些操作涉及到3个数据库更新:
这三个不同的操作作为一个单元并自动执行是至关重要的。三个操作必须全部成功,或都不成功--任何一个操作出现误差都将破坏数据完整性。事务处理提供了这种完整性及其它保证。
要进一步了解事务处理过程的基本原则,见http://msdn.microsoft.com/library/en-us/cpguide/html/cpcontransactionprocessingfundamentals.asp。
可以采用很多方法将事务管理合并到数据访问代码中。每种方法适合下面两种基本编程模型之一。
尽管自动化事务处理模型极大地简化了分布式事务处理过程,但两种模型都用于执行本地事务处理(即对单个资源管理器如SQL Server 2000执行的事务处理)或分布式事务处理(即,对位于远程计算机上的多个资源管理执行的事务处理)。
你也许会试图利用自动化(COM+)事务处理来从易于编程的模型中获益。在有多个组件执行数据库更新的系统中,这种优点更明显。然而,在很多情况下,应当避免这种事务处理模型所带来的额外开销和性能损失。
本节将指导你根据特定的应用程序环境选择最合适的模型。
选择事务处理模型
在选择事务处理模型前,首先应当考虑是否真正需要事务处理。事务处理是服务器应用程序使用的最昂贵的资源,在不必要使用的地方,它们降低了扩展性。考虑下面用于管理事务处理使用的准则:
自动化事务处理与手工事务处理的对比
尽管编程模型已经对自动化事务处理进行了简化,特别是在多个组件执行数据库更新时,但本地事务处理总是相当快,因为它们不需要与微软DTC交互。即使你对单个本地资源管理器(如SQL Server)使用自动化事务处理,也是这种情况(尽管性能损失减少了),因为手式本地事务处理避免了所有不必要的与DTC的进程间通信。
对于下面的情况,需使用手工事务处理:
对于下列情况,则宜使用自动事务处理:
注意 避免混用事务处理模型。最好只使用其中一个。
在性能足够好的应用程序环境中,(甚至对于单个数据库)选择自动化事务处理以简化编程模型,这种做法是合理的。自动化事务处理使多个组件能很容易地执行现一事务处理中的多个操作。
使用手工事务处理
对于手工事务处理,可以直接在组件代码或存储过程中分别编写使用ADO.NET 或 Transact-SQL事务处理支持特性的代码。多数情况下,应选择在存储过程中控制事务处理,因为这种方法提供了更高的封装性,并且在性能方面,此方法与利用ADO.NET 代码执行事务处理兼容。
利用ADO.NET执行手工事务处理
ADO.NET支持事务处理对象,利用此对象可以开始新事务处理过程,并明确控制事务处理是否执行还是回滚。事务处理对象与单个数据库链接相关,可以通过链接对象的BeginTransaction方法获得。调用此方法并不是暗示,接下来的命令是在事务处理上下文中发出的。必须通过设置命令的Transaction属性,明确地将每个命令与事务处理关联起来。可以将多个命令对象与事务处理对象关联,因此在单个事务处理中就针对单个数据库把多个操作进行分组。
关于使用ADO.NET事务处理代码的示例,见附录中如何编码ADO.NET手工事务处理。
更多信息
利用存储过程执行手工事务处理
也可以在存储过程中使用Transact-SQL语句直接控制手工事务处理。例如,可以利用包含了Transact-SQL事务处理语句(如BEGIN TRANSACTION、END TRANSACTION及ROLLBACK TRANSACTION)的存储过程执行事务处理。
更多信息
使用自动化事务
自动化事务简化了编程模型,因为它们不需要明确地开始新事务处理过程,或明确执行或取消事务。然而,自动化事务的最大优点是它们能与DTC结合起来,这就使单个事务可以扩展到多个分布式数据源中。在大型分布式应用程序中,这个优点是很重要的。尽管通过手工对DTC直接编程来控制分布式事务是可能的,但自动化事务处理极大的简化了工作量,并且它是为基于组件的系统而设计的。例如,可以方便地以说明方式配置多个组件以执行包含了单个事务处理的任务。
自动化事务依赖于COM+提供的分布式事务处理支持特性。结果,只有服务组件(即从ServicedComponent类中派生的组件)能够使用自动化事务。
要为自动化事务处理配置类,操作如下:
更多信息
配置事务处理分离级别
用于COM+1.0版--即运行在Windows 2000中的COM+--的事务处理分离级别被序列化了。这样做提供了最高的分离等级,却是以性能为代价的。系统的整体吞吐量被降低了。因为所涉及到的资源管理器(典型地是数据库)在事务处理期间必须保持读和写锁。在此期间,其它所有事务处理都被阻断了,这种情况将对应用程序的扩展能力产生极大冲击。
随微软Windows .NET发行的COM+ 1.5版允许有COM+目录中按组件配置事务处理分离等级。与事务中根组件相关的设置决定了事务处理的分离等级。另外,同一事务流中的内部子组件拥有的事务处理等级必须不能高于要组件所定义的等级。如果不是这样,当子组件实例化时,将导致错误。
对.NET管理类,Transaction属性支持所有的公有Isolation属性。你可以用此属性陈述式地指定一特殊分离等级,如下面的代码所示:
[Transaction(TransactionOption.Supported, Isolation=TransactionIsolationLevel.ReadCommitted)] public class Account : ServicedComponent { . . . }
更多信息
关于配置事务处理分离等级及其它Windows .NET COM+增强特性的更多信息,见MSDN杂志2001年8月期的“Windows XP:利用COM+ 1.5的增强特性使你的组件更强壮”一文。
确定事务处理结果
在单个事务流的所有事务处理组件上下文中,自动化事务处理结果由事务取消标志和一致性标志的状态决定。当事务流中的根组件成为非活动状态(并且控制权返回调用者)时,确定事务处理结果。这种情况在图5中得到了演示,此图显示的是一个典型的银行基金传送事务。
图5 事务流上下文
当根对象(在本例中是对象)变为非活动状态,并且客户的方法调用返回时,确定事务处理结果。在任何上下文中的任何一致性标志被设为假,或如果事务处理取消标志设为真,那么底层的物理DTC事务将被取消。
可以以下面两种方式之一从.NET对象中控制事务处理结果:
严重性大于10的SQL Server错误将导致管理数据供应器释放SqlException类型的异常。如果方法缓存并处理异常,就要确保或者通过手工取消了事务,或者方法被标记了[AutoComplete],以保证异常能传递回调用者。
AutoComplete方法
对于标记了属性的方法,执行下面操作:
异常如果不能传递,将导致对象不会提出取消事务,从而忽视数据库错误。这意味着共享同一事务流的其它对象的成功操作将被提交。
下面的代码缓存了SqlException,然后将它直接传递回调用者。事务处理最终将被取消,因为对象的一致性标志在对象变为非活动状态时自动被设为假。
[AutoComplete] void SomeMethod() { try { // Open the connection, and perform database operation . . . } catch (SqlException sqlex ) { LogException( sqlex ); // Log the exception details throw; // Rethrow the exception, causing the consistent // flag to be set to false. } finally { // Close the database connection . . . } }
Non-AutoComlete方法
对于没有AutoComplete的属性的方法,必须:
代码说明了这种方法。
void SomeOtherMethod() { try { // Open the connection, and perform database operation . . . ContextUtil.SetComplete(); // Manually vote to commit the transaction } catch (SqlException sqlex) { LogException( sqlex ); // Log the exception details ContextUtil.SetAbort(); // Manually vote to abort the transaction // Exception is handled at this point and is not propagated to the caller } finally { // Close the database connection . . . } }
注意 如果有多个catch块,在方法开始的时候调用ContextVtil.SetAbort,以及在try块的末尾调用ContextUtil.SetComplete都会变得容易。用这种方法,就不需要在每个catch块中重复调用ContextUtil.SetAbort。通过这种方法确定的相容标志的设置只在方法返回时有效。
对于异常事件(或循环异常),必须把它传递到调用堆栈中,因为这使得调用代码认为事务处理失败。它允许调用代码做出优化选择。比如,在银行资金转账中,如果债务操作失败,则转帐分支可以决定不执行债务操作。
如果把相容标志设置为假并且在返回时没有出现异常事件,则调用代码就没有办法知道事务处理是否一定失败。虽然可以返回Boolean值或设置Boolean输出参数,但还是应该前后一致,通过显示异常事件以表明有错误发生。这样代码就有一种标准的错误处理方法,因此更简明、更具有相容性。
在分布式应用程序中利用数据进行分页是一项普遍的要求。比如,用户可能得到书的列表而该列表又不能够一次完全显示,用户就需要在数据上执行一些熟悉的操作,比如浏览下一页或上一页的数据,或者跳到列表的第一页或最后一页。
这部分内容将讨论实现这种功能的选项,以及每种选项在性能和缩放性上的效果。
选项比较
数据分页的选项有:
对数据进行分页的最优选项依赖于下列因素:
性能测试表明利用存储过程的手工方法在很大的应力水平范围上都提供了最佳性能。然而,由于手工方法在服务器上执行工作,如果大部分站点功能都依赖数据分页功能,那么服务器性能就会成一个关键要素。为确保这种方法能适合特殊环境,应该测试各种特殊要求的选项。
下面将讨论各种不同的选项。
使用SqlDataAdapter
如前面所讨论的,SqlDataAdapter是用来把来自数据库的数据填充到DataSet中,过载的Fill方法中的任一个都需要两个整数索引值(如下列代码所示):
public int Fill( DataSet dataSet, int startRecord, int maxRecords, string srcTable );
StartRecord值标示从零开始的记录起始索引值。MaxRecord值表示从startRecord开始的记录数,并将拷贝到新的DataSet中。
SqlDataAdapter在内部利用SqlDataReader执行查询并返回结果。SqlDataAdapter读取结果并创建基于来自SalDataReader的数据的Dataset。SqlDataAdapter通过startRecord和maxRecords把所有结果都拷贝到新生成的DataSet中,并丢弃不需要的数据。这意味着许多不必要的数据将潜在的通过网络进入数据访问客户--这是这种方法的主要缺陷。
比如,如果有1000个记录,而需要的是第900到950个记录,那么前面的899个记录将仍然穿越网络然后被丢弃。对于小数量的记录,这种开销可能是比较小的,但如果针对大量数据的分页,则这种开销就会非常巨大。
使用ADO
实现分页的另一个选项是利用基于COM的ADO进行分页。这种方法的目标是获得访问服务器光标。服务器光标通过ADO Recordset对象显示。可以把Recordset光标的位置设置到adUseServer中。如果你的OLE DB供应器支持这种设置(如SQLOLEDB那样),就可以使用服务器光标。这样就可以利用光标直接导航到起始记录,而不需要将所有数据传过网络进入访问数据的用户代码中。
这种方法有下面两个缺点:
提供手工实现
在本部分中讨论的数据分页的最后一个选项是利用存储过程手工实现应用程序的分页功能。对于包含唯一关键字的表格,实现存储过程相对容易一些。而对于没有唯一关键字的表格(也不应该有许多关键字),该过程会相对复杂一些。
带有唯一关键字的表格的分页
如果表格包含一个唯一关键字,就可以利用WHERE条款中的关键字创建从某个特殊行起始的结果设置。这种方法,与用来限制结果设置大小的SET ROWCOUNT状态是相匹配的,提供了一种有效的分页原理。这一方法将在下面存储的代码中说明:
CREATE PROCEDURE GetProductsPaged @lastProductID int, @pageSize int AS SET ROWCOUNT @pageSize SELECT * FROM Products WHERE [standard search criteria] AND ProductID > @lastProductID ORDER BY [Criteria that leaves ProductID monotonically increasing] GO
这个存储过程的调用程序仅仅维护LastProductID的值,并通过所选的连续调用之间的页的大小增加或减小该值。
不带有唯一关键字的表格的分页
如果需要分页的表格没有唯一关键字,可以考虑添加一个--比如利用标识栏。这样就可以实现上面讨论的分页方案了。
只要能够通过结合结果记录中的两个或更多区域来产生唯一性,就仍然有可能实现无唯一关键字表格的有效分页方案。
比如,考察下列表格:
Col1 | Col2 | Col3 | Other columns… |
A | 1 | W | … |
A | 1 | X | . |
A | 1 | Y | . |
A | 1 | Z | . |
A | 2 | W | . |
A | 2 | X | . |
B | 1 | W | … |
B | 1 | X | . |
对于该表,结合Col 、Col2 和Col3就可能产生一种唯一性。这样,就可以利用下面存储过程中的方法实现分布原理:
CREATE PROCEDURE RetrieveDataPaged @lastKey char(40), @pageSize int AS SET ROWCOUNT @pageSize SELECT Col1, Col2, Col3, Col4, Col1+Col2+Col3 As KeyField FROM SampleTable WHERE [Standard search criteria] AND Col1+Col2+Col3 > @lastKey ORDER BY Col1 ASC, Col2 ASC, Col3 ASC GO
客户保持存储过程返回的keyField栏的最后值,然后又插入回到存储过程中以控制表的分页。
虽然手工实现增加了数据库服务器上的应变,但它避免了在网络上传输不必要的数据。性能测试表明在整个应变水平中这种方法都工作良好。然而,根据站点工作所涉及的数据分页功能的多少,在服务器上进行手工分页可能影响应用程序的可扩展性。应该在所在环境中运行性能测试,为应用程序找到最合适的方法。
附录
如何为一个.NET类启用对象结构
要利用Enterprise (COM+)Services为对象结构启用.NET管理的类,需要执行下列步骤:
using System.EnterpriseServices; public class DataAccessComponent : ServicedComponent
[ConstructionEnabled(Default="default DSN")] public class DataAccessComponent : ServicedComponent
public override void Construct( string constructString ) { // Construct method is called next after constructor. // The configured DSN is supplied as the single argument }
[assembly: AssemblyKeyFile("DataServices.snk")]
// the ApplicationName attribute specifies the name of the // COM+ Application which will hold assembly components [assembly : ApplicationName("DataServices")] // the ApplicationActivation.ActivationOption attribute specifies // where assembly components are loaded on activation // Library : components run in the creator's process // Server : components run in a system process, dllhost.exe [assembly: ApplicationActivation(ActivationOption.Library)]
下列代码段是一个叫做DataAccessComponent的服务组件,它利用COM+结构字符串来获得数据库连接字符串。
using System; using System.EnterpriseServices; // the ApplicationName attribute specifies the name of the // COM+ Application which will hold assembly components [assembly : ApplicationName("DataServices")] // the ApplicationActivation.ActivationOption attribute specifies // where assembly components are loaded on activation // Library : components run in the creator's process // Server : components run in a system process, dllhost.exe [assembly: ApplicationActivation(ActivationOption.Library)] // Sign the assembly. The snk key file is created using the // sn.exe utility [assembly: AssemblyKeyFile("DataServices.snk")] [ConstructionEnabled(Default="Default DSN")] public class DataAccessComponent : ServicedComponent { private string connectionString; public DataAccessComponent() { // constructor is called on instance creation } public override void Construct( string constructString ) { // Construct method is called next after constructor. // The configured DSN is supplied as the single argument this.connectionString = constructString; } }
如何利用SqlDataAdapter来检索多个行
下面的代码说明如何利用SqlDataAdapter对象发出一个生成Data Set或Datatable的命令。它从SQL Server Northwind数据库中检索一系列产品目录。
using System.Data; using System.Data.SqlClient; public DataTable RetrieveRowsWithDataTable() { using ( SqlConnection conn = new SqlConnection(connectionString) ) { SqlCommand cmd = new SqlCommand("DATRetrieveProducts", conn); cmd.CommandType = CommandType.StoredProcedure; SqlDataAdapter da = new SqlDataAdapter( cmd ); DataTable dt = new DataTable("Products"); da.Fill(dt); return dt; } }
按下列步骤利用SqlAdapter生成DataSet或DataTable:
如何利用SqlDataReader检索多个行
下列代码说明了如何利用SqlDataReader方法检索多行:
using System.IO; using System.Data; using System.Data.SqlClient; public SqlDataReader RetrieveRowsWithDataReader() { SqlConnection conn = new SqlConnection( "server=(local);Integrated Security=SSPI;database=northwind"); SqlCommand cmd = new SqlCommand("DATRetrieveProducts", conn ); cmd.CommandType = CommandType.StoredProcedure; try { conn.Open(); // Generate the reader. CommandBehavior.CloseConnection causes the // the connection to be closed when the reader object is closed return( cmd.ExecuteReader( CommandBehavior.CloseConnection ) ); } catch { conn.Close(); throw; } } // Display the product list using the console private void DisplayProducts() { SqlDataReader reader = RetrieveRowsWithDataReader(); while (reader.Read()) { Console.WriteLine("{0} {1} {2}", reader.GetInt32(0).ToString(), reader.GetString(1) ); } reader.Close(); // Also closes the connection due to the // CommandBehavior enum used when generating the reader }
按下列步骤利用SqlDataReader检索多行:
如何利用XmlReader检索多个行
可以利用SqlCommand对象生成XmlReader对象,它提供对XML数据的基于流的前向访问。该命令(通常是一个存储的过程)必须生成一个基于XML的结果设置,它对于SQL Server2000通常是由带有有效条款FOR XML的SELECT状态组成。下列代码段说明了这种方法:
public void RetrieveAndDisplayRowsWithXmlReader() { SqlConnection conn = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand("DATRetrieveProductsXML", conn ); cmd.CommandType = CommandType.StoredProcedure; try { conn.Open(); XmlTextReader xreader = (XmlTextReader)cmd.ExecuteXmlReader(); while ( xreader.Read() ) { if ( xreader.Name == "PRODUCTS" ) { string strOutput = xreader.GetAttribute("ProductID"); strOutput += " "; strOutput += xreader.GetAttribute("ProductName"); Console.WriteLine( strOutput ); } } xreader.Close(); } catch { throw; } finally { conn.Close(); } }
上述代码使用了下列存储过程:
CREATE PROCEDURE DATRetrieveProductsXML AS SELECT * FROM PRODUCTS FOR XML AUTO GO
按下列步骤检索XML数据:
如何利用存储过程输出参数检索单个行
可以调用一个存储过程,它通过一种称做输出参数的方式可以在单个行中返回检索数据项。下列代码段利用存储的过程检索产品的名称和单价,该产品包含在Northwind数据库中。
void GetProductDetails( int ProductID, out string ProductName, out decimal UnitPrice ) { SqlConnection conn = new SqlConnection( "server=(local);Integrated Security=SSPI;database=Northwind"); // Set up the command object used to execute the stored proc SqlCommand cmd = new SqlCommand( "DATGetProductDetailsSPOutput", conn ); cmd.CommandType = CommandType.StoredProcedure; // Establish stored proc parameters. // @ProductID int INPUT // @ProductName nvarchar(40) OUTPUT // @UnitPrice money OUTPUT // Must explicitly set the direction of output parameters SqlParameter paramProdID = cmd.Parameters.Add( "@ProductID", ProductID ); paramProdID.Direction = ParameterDirection.Input; SqlParameter paramProdName = cmd.Parameters.Add( "@ProductName", SqlDbType.VarChar, 40 ); paramProdName.Direction = ParameterDirection.Output; SqlParameter paramUnitPrice = cmd.Parameters.Add( "@UnitPrice", SqlDbType.Money ); paramUnitPrice.Direction = ParameterDirection.Output; try { conn.Open(); // Use ExecuteNonQuery to run the command. // Although no rows are returned any mapped output parameters // (and potentially return values) are populated cmd.ExecuteNonQuery( ); // Return output parameters from stored proc ProductName = paramProdName.Value.ToString(); UnitPrice = (decimal)paramUnitPrice.Value; } catch { throw; } finally { conn.Close(); } }
按下列步骤利用存储的过程输出参数检索单个行:
注意 明确设置所有参数的方向是一次很好的练习,包括输入参数。
上述代码段启用了下列存储过程。
CREATE PROCEDURE DATGetProductDetailsSPOutput @ProductID int, @ProductName nvarchar(40) OUTPUT, @UnitPrice money OUTPUT AS SELECT @ProductName = ProductName, @UnitPrice = UnitPrice FROM Products WHERE ProductID = @ProductID GO
如何利用SqlDataReader检索单个行
可以利用SqlDataReader对象检索单个行,以及来自返回数据流的所需栏的值。这由下列代码说明:
void GetProductDetailsUsingReader( int ProductID, out string ProductName, out decimal UnitPrice ) { SqlConnection conn = new SqlConnection( "server=(local);Integrated Security=SSPI;database=Northwind"); // Set up the command object used to execute the stored proc SqlCommand cmd = new SqlCommand( "DATGetProductDetailsReader", conn ); cmd.CommandType = CommandType.StoredProcedure; // Establish stored proc parameters. // @ProductID int INPUT SqlParameter paramProdID = cmd.Parameters.Add( "@ProductID", ProductID ); paramProdID.Direction = ParameterDirection.Input; try { conn.Open(); SqlDataReader reader = cmd.ExecuteReader(); reader.Read(); // Advance to the one and only row // Return output parameters from returned data stream ProductName = reader.GetString(0); UnitPrice = reader.GetDecimal(1); reader.Close(); } catch { throw; } finally { conn.Close(); } }
按下列步骤返回带有SqlDataReader对象:
上述代码段启用了下列存储过程:
CREATE PROCEDURE DATGetProductDetailsReader @ProductID int AS SELECT ProductName, UnitPrice FROM Products WHERE ProductID = @ProductID GO
如何利用ExecuteScalar单个项
ExecuteScalar方法是设计成用于返回单个值的访问。在返回多列或多行的访问事件中,ExecuteScalar只返回第一行的第一例。
下列代码说明如何查询某个产品ID的产品名称:
void GetProductNameExecuteScalar( int ProductID, out string ProductName ) { SqlConnection conn = new SqlConnection( "server=(local);Integrated Security=SSPI;database=northwind"); SqlCommand cmd = new SqlCommand("LookupProductNameScalar", conn ); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add("@ProductID", ProductID ); try { conn.Open(); ProductName = (string)cmd.ExecuteScalar(); } catch { throw; } finally { conn.Close(); } }
按下列步骤利用Execute Scalar检索单个项:
上述代码启用了下列存储过程:
CREATE PROCEDURE LookupProductNameScalar @ProductID int AS SELECT TOP 1 ProductName FROM Products WHERE ProductID = @ProductID GO
如何利用存储过程输出或返回的参数检索单个项
利用存储过程输出或返回的参数可以查询单个值,下列代码说明了输出参数的使用:
void GetProductNameUsingSPOutput( int ProductID, out string ProductName ) { SqlConnection conn = new SqlConnection( "server=(local);Integrated Security=SSPI;database=northwind"); SqlCommand cmd = new SqlCommand("LookupProductNameSPOutput", conn ); cmd.CommandType = CommandType.StoredProcedure; SqlParameter paramProdID = cmd.Parameters.Add("@ProductID", ProductID ); ParamProdID.Direction = ParameterDirection.Input; SqlParameter paramPN = cmd.Parameters.Add("@ProductName", SqlDbType.VarChar, 40 ); paramPN.Direction = ParameterDirection.Output; try { conn.Open(); cmd.ExecuteNonQuery(); ProductName = paramPN.Value.ToString(); } catch { throw; } finally { conn.Close(); } }
按下列步骤利用存储过程的输出参数检索单个值:
上述代码使用了下列存储过程:
CREATE PROCEDURE LookupProductNameSPOutput @ProductID int, @ProductName nvarchar(40) OUTPUT AS SELECT @ProductName = ProductName FROM Products WHERE ProductID = @ProductID GO
下列代码说明如何利用返回值确定是否存在特殊行。从编码的角度看,这与使用存储过程输出参数相类似,除了需要明确设置到ParameterDirection.ReturnValue的SqlParameter方向。
bool CheckProduct( int ProductID ) { SqlConnection conn = new SqlConnection( "server=(local);Integrated Security=SSPI;database=northwind"); SqlCommand cmd = new SqlCommand("CheckProductSP", conn ); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add("@ProductID", ProductID ); SqlParameter paramRet = cmd.Parameters.Add("@ProductExists", SqlDbType.Int ); paramRet.Direction = ParameterDirection.ReturnValue; try { conn.Open(); cmd.ExecuteNonQuery(); } catch { throw; } finally { conn.Close(); } return (int)paramRet.Value == 1; }
按下列步骤,可以利用存储过程返回值检查是否存在特殊行:
上述代码使用了下列存储过程:
CREATE PROCEDURE CheckProductSP @ProductID int AS IF EXISTS( SELECT ProductID FROM Products WHERE ProductID = @ProductID ) return 1 ELSE return 0 GO
如何利用SqlDataReader检索单个项。
通过调用命令对象的ExecuteReader方法,可以利用SqlDataReader对象获得单个输出值。这需要稍微多一些的代码,因为SqlDataReader Read方法必须调用,然后所需值通过读者存取程序方法得到检索。SqlDataReader对象的使用在下列代码中说明:
bool CheckProductWithReader( int ProductID ) { SqlConnection conn = new SqlConnection( "server=(local);Integrated Security=SSPI;database=northwind"); SqlCommand cmd = new SqlCommand("CheckProductExistsWithCount", conn ); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add("@ProductID", ProductID ); cmd.Parameters["@ProductID"].Direction = ParameterDirection.Input; try { conn.Open(); SqlDataReader reader = cmd.ExecuteReader( CommandBehavior.SingleResult ); reader.Read(); bool bRecordExists = reader.GetInt32(0) > 0; reader.Close(); return bRecordExists; } catch { throw; } finally { conn.Close(); } }
上述代码使用了下列存储过程:
CREATE PROCEDURE CheckProductExistsWithCount @ProductID int AS SELECT COUNT(*) FROM Products WHERE ProductID = @ProductID GO
如何编码ADO.NET手工事务
下列代码说明如何利用SQL Server. NET数据供应器提供的事务支持来保护事务的支金转帐操作。该操作在位于同一数据库中的两个帐户之间转移支金。
public void TransferMoney( string toAccount, string fromAccount, decimal amount ) { using ( SqlConnection conn = new SqlConnection( "server=(local);Integrated Security=SSPI;database=SimpleBank" ) ) { SqlCommand cmdCredit = new SqlCommand("Credit", conn ); cmdCredit.CommandType = CommandType.StoredProcedure; cmdCredit.Parameters.Add( new SqlParameter("@AccountNo", toAccount) ); cmdCredit.Parameters.Add( new SqlParameter("@Amount", amount )); SqlCommand cmdDebit = new SqlCommand("Debit", conn ); cmdDebit.CommandType = CommandType.StoredProcedure; cmdDebit.Parameters.Add( new SqlParameter("@AccountNo", fromAccount) ); cmdDebit.Parameters.Add( new SqlParameter("@Amount", amount )); conn.Open(); // Start a new transaction using ( SqlTransaction trans = conn.BeginTransaction() ) { // Associate the two command objects with the same transaction cmdCredit.Transaction = trans; cmdDebit.Transaction = trans; try { cmdCredit.ExecuteNonQuery(); cmdDebit.ExecuteNonQuery(); // Both commands (credit and debit) were successful trans.Commit(); } catch( Exception ex ) { // transaction failed trans.Rollback(); // log exception details . . . throw ex; } } } }
如何利用Transact-SQL执行事务
下列存储过程说明了如何在Transact-SQL过程内执行事务的支金转移操作。
CREATE PROCEDURE MoneyTransfer @FromAccount char(20), @ToAccount char(20), @Amount money AS BEGIN TRANSACTION -- PERFORM DEBIT OPERATION UPDATE Accounts SET Balance = Balance - @Amount WHERE AccountNumber = @FromAccount IF @@RowCount = 0 BEGIN RAISERROR('Invalid From Account Number', 11, 1) GOTO ABORT END DECLARE @Balance money SELECT @Balance = Balance FROM ACCOUNTS WHERE AccountNumber = @FromAccount IF @BALANCE < 0 BEGIN RAISERROR('Insufficient funds', 11, 1) GOTO ABORT END -- PERFORM CREDIT OPERATION UPDATE Accounts SET Balance = Balance + @Amount WHERE AccountNumber = @ToAccount IF @@RowCount = 0 BEGIN RAISERROR('Invalid To Account Number', 11, 1) GOTO ABORT END COMMIT TRANSACTION RETURN 0 ABORT: ROLLBACK TRANSACTION GO
该存储过程使用BEGIN TRANSACTION, COMMIT TRANSACTION,和ROLLBACK TRANSACTION状态手工控制事务。
如何编码事务性的.NET类
下述例子是三种服务性的NET类,它们配置或用于自动事务。每个类都带有Transaction属性,它的值将决定是否启动新事务流或者对象是否共享即时调用程序的数据流。这些元素一起工作来执行银行支金转移。Transfer类配置有RequiresNew事务属性,而Debit和Credit类配置有Required属性。这样,在运行的时候三个对象共享同一个事务。
using System; using System.EnterpriseServices; [Transaction(TransactionOption.RequiresNew)] public class Transfer : ServicedComponent { [AutoComplete] public void Transfer( string toAccount, string fromAccount, decimal amount ) { try { // Perform the debit operation Debit debit = new Debit(); debit.DebitAccount( fromAccount, amount ); // Perform the credit operation Credit credit = new Credit(); credit.CreditAccount( toAccount, amount ); } catch( SqlException sqlex ) { // Handle and log exception details // Wrap and propagate the exception throw new TransferException( "Transfer Failure", sqlex ); } } } [Transaction(TransactionOption.Required)] public class Credit : ServicedComponent { [AutoComplete] public void CreditAccount( string account, decimal amount ) { SqlConnection conn = new SqlConnection( "Server=(local); Integrated Security=SSPI"; database="SimpleBank"); SqlCommand cmd = new SqlCommand("Credit", conn ); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add( new SqlParameter("@AccountNo", account) ); cmd.Parameters.Add( new SqlParameter("@Amount", amount )); try { conn.Open(); cmd.ExecuteNonQuery(); } catch (SqlException sqlex) { // Log exception details here throw; // Propagate exception } } } [Transaction(TransactionOption.Required)] public class Debit : ServicedComponent { public void DebitAccount( string account, decimal amount ) { SqlConnection conn = new SqlConnection( "Server=(local); Integrated Security=SSPI"; database="SimpleBank"); SqlCommand cmd = new SqlCommand("Debit", conn ); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add( new SqlParameter("@AccountNo", account) ); cmd.Parameters.Add( new SqlParameter("@Amount", amount )); try { conn.Open(); cmd.ExecuteNonQuery(); } catch (SqlException sqlex) { // Log exception details here throw; // Propagate exception back to caller } } }
合作者
非常感谢下列撰稿者和审校者:
Bill Vaughn, Mike Pizzo, Doug Rothaus, Kevin White, Blaine Dokter, David Schleifer, Graeme Malcolm(内容专家), Bernard Chen(西班牙人), Matt Drucke(协调)和Steve kirk.
读者有什么样的问题、评论和建议?关于本文的反馈信息,请发E-mail至devfdbck®microsoft.com。
你希望学习并利用.NET的强大功能吗?与微软技术中心的技术专家一起工作,学习开发最佳方案。详细信息请访问: http://www.micrsoft.com/business/services/mtc.asp。