Alex Mackman, Chris Brooks, Steve Busby, 和 Ed Jezierski
微软公司
2001年10月
概述:本文提供了在多层.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只发行了两个供应器:
- SQL Server .NET 数据供应器。这是用于Microsoft SQL Server 7.0及其以后版本数据库的供应器,它优化了对SQL Server的访问,并利用 SQL Server内置的数据转换协议直接与SQL Server通信。
- 当链接到SQL Server 7.0 或 SQL Server 2000时,总是要使用此供应器。
- OLE DB .NET 数据供应器。. 这是一个用于管理OLE DB 数据源的供应器。它的效率稍低于SQL Server .NET Data Provider,因为在与数据库通信时,它需通过OLE DB层进行呼叫。注意,此供应器不支持用于开放数据库链接(ODBC),MSDASQL的OLE DB供应器。对于ODBC数据源,应使用ODBC .NET数据供应器。有关与ADO.NET兼容的OLE DB供应器列表,见 。
目前测试版中的其它.NET数据供应器包括:
- ODBC .NET 数据供应器。目前Beta 1.0版可供下载。它提供了对ODBC驱动器的内置访问,其方式与OLE DB .NET数据供应器提供的对本地OLE DB供应器的访问方式相同。关于ODBC .NET及Beta版下载的更多信息见 .
- 用于从SQL Server 2000中得到XML的管理供应器。用于SQL Server Web升级2版的XML还包括了专用于从SQL Server 2000中得到XML的管理供应器。关于此升级版本的更多信息,见 .
名称空间组织
与每个.NET数据供应器相关的类型(类,结构,枚举,等等)位于它们各自的名称空间中:
- System.Data.SqlClient. 包含了 SQL Server .NET 数据供应器类型。
- System.Data.OleDb. 包含了 OLE DB .NET数据供应器类型。
- System.Data.Odbc. 包含了ODBC .NET数据供应器类型。
- System.Data. 包含了独立于供应器的类型,如DataSet及DataTable。
在各自关联的名称空间中,每个供应器都提供了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语句的原因是:
- 存储过程通常会使性能增加,因为数据库可以优化过程使用的数据访问计划,并对其进行缓存以备将来重用。
- 在数据库中,存储过程可分别得到保护。客户可以被给予执行某个存储过程的权限,但无权处理底层的表。
- 存储过程将导致维护简单,因为在一个已部署组件内,修改存储过程通常要比修改硬编码的SQL语句简单。
- 存储过程增加了一个从底层的数据库结构中提取出的层。存储过程的客户与存储过程的实现细节及底层结构被隔离开了。
- 存储过程可以降低网络流量,因为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应用程序中。你必须正确管理你的链接,因为你的方法将极大的影响应用程序的整体升级性。还有,必须仔细考虑在哪儿存放链接字符串。你需要一个可配置的、安全的位置。
在管理数据库链接和链接字符串时,你应当努力:
- 通过跨多个客户多路复用一池数据库链接来帮助实现应用程序的扩展性。
- 采用可配置的、高性能的链接池战略。
- 在访问SQL Server时使用微软Windows操作系统认证。
- 避免中间层的冒充。
- 安全地存储链接字符串。
- 较晚地打开数据库链接,而较早地关闭它们。
本节讨论链接池,并帮你选择合适的链接池战略。其它可选方法也是存在的。本节也将考虑如何管理、存储、控制数据库链接字符串。最后,本节还提供了两个编码方案,使用它们将有助于确保链接已可靠关闭,并返回到链接池中。
链接池
数据库链接池使应用程序能够重用池中的现有链接,而不是重复地建立对数据库的链接。这种技术将极大地增加应用程序的可扩展性,因为有限的数据库链接可以为很多的客户提供服务。此技术也将提高性能,因为能够避免用于建立新链接的巨大时间。
数据访问技术,如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)
- 在.NET框架Beta版中,当在调试器中运行时,链接池化总是失效了。在调试器外,对调试版和发行版,链接池都能正常运作。.NET框架的最终发行版(RTM)取消了这种限制,链接池在所有情况下都能运行。
- 链接池被划分为了多个特定于事务的池和一个用于目前没有列在事务中的多个链接的池。对于与特定事务上下文相关的线程,将从(包含了与事务建立的链接的)合适的池中返回链接。这使得使用已建立的链接成为透明过程。
用OLE DB .NET数据供应器池化
OLE DB .NET数据供应器利用OLE DB资源池化的底层服务将链接存储到池中。很多方法可用于配置资源池化:
- 可以使用链接字符串来配置、使能资源池化或使其使失效。
- 可以使用注册表。
- 可以通过程序来配置资源池化。
为了避开与注册表相关的部署问题,应避免使用注册表配置OLE DB资源池化。
关于OLE DB 资源池化的更多细节,见MSDN中“OLE DB程序员参考”一书的第19章:OLE DB服务中的资源池化部分。
用池化对象管理链接池化
作为Windows DNA开发人员,建议你使OLE DB资源池化和/或ODBC链接池化失效,并把COM+对象池化用作将数据库链接存储到池中的技术。这样做主要出于两个原因:
- 池容量和极限可以(在COM+目录)被明确配置。
- 性能提高了。池化对象的方法可以成倍的胜过固有池化。
然而,由于SQL Server .NET数据供应器内置地使用池化,所以(在使用此供应器时)你不再需要开发自己的对象池化机制。这样就可以避免手工事务征募带来的复杂性。
如果正在使用OLE DB .NET数据供应器,那么考虑COM+对象池化以从高级配置和改进的性能中受益。如果你为此目的开发一个池化对象,那么必须使用OLE DB资源池化和自动事务征募失效(例如,通过将“OLE DB Services=-4”包含进链接字符串中)。必须在池化对象的实现中处理事务征募。
监视链接池化
要监视应用程序对链接池化的应用情况,可以使用随SQL Server发行的Profiler工具,或随微软Windows 2000发行的性能监视器。
要利用SQL Server Profiler 监视链接池化,操作如下:
- 单击开始,指向程序,指向Microsoft SQL Server,然后单击Profiler运行Profiler。
- 在文件菜单中,指向新建,然后单击跟踪。
- 提供链接内容,然后单击确定。
- 在跟踪属性对话框中,单击事件标签。
- 在已选事件类别列表中,确保审核登录和审核登出事件显示在安全审核下面。
- 单击运行开始跟踪。在链接建立时,将会看到审核登录事件;在链接关闭时看到审核登出事件。
要通过性能监视器监视链接池化,操作如下:
- 单击开始,指向程序,指向管理工具,然后单击性能运行性能监视器。
- 在图表背景中右击,然后单击增加计数器。
- 在性能对象下拉列表框中,单击SQL Server:通用统计。
- 在出现的列表中,单击用户链接。
- 单击增加,然后单击关闭。
注意 .NET框架的RTM版本将另外包含一组ADO .NET性能计数器(这些计数器能与性能监视器结合起来使用),这些计数器用于为SQL Server .NET数据供应器监视并积累链接池化状态。
管理安全性
尽管数据库链接池化提高了应用程序的整体扩展性,这也意味着你不再能够在数据库端管理安全性。这是因为为了支持链接池化,链接字符串必须是相同的。如果需要跟踪每个用户的数据库操作,那么考虑为每个操作增加一个参数,通过这个参数就可以传递用户身份,手工将用户活动记入数据库。
使用Windows 认证
在链接到SQL Server时,应当使用Windows认证,因为它提供了许多优点:
- 安全性易于管理,因为使用了单一(Windows)安全模型而不是分散的SQL Server安全模型。
- 避免了在链接字符串中嵌入用户名和密码。
- 用户名和密码不是以明文方式在网络中传输的。
- 通过密码过期期限,最小长度,多次无效登录请求后帐号锁定提高了登录的安全性。
.NETBeta 2版的性能测试表明,使用Windows认证与使用SQL Server认证相比,要花费更多的时间才能打开池化的数据库链接。然而,尽管Windows认证的成本较高,但与执行一个命令或存储过程所花费的时间相比,其(引起的)性能损失相对来说并不重要。结果,上面所列出的Windows认证的优点通常会稍微超过性能损失。
同样,当打开一个池化链接时,在.NET框架的RTM版本中,Windows认证与SQL Server认证的差别有望变得更不明显。
避免在中间层中冒充
Windows认证需要访问数据库的Windows帐号。虽然看上去在中间层中使用冒充更符合逻辑,但必须避免这样做,因为损害链接池化并对应用程序的扩展性产生严重影响。
为了解决这个问题,考虑对有限的Windows帐号(而不是被认证的负责人)实施冒充,每个帐号代表一个特定的角色。
例如,可以考虑下面的方法:
- 创建两个Windows帐号,一个用于读操作,一个用于写操作(也可以用单独的帐号映射针对特定应用程序的角色。例如,可以为互联网用户使用一个帐号,而为内部操作员和/或管理员使用另外的帐号)。
- 将每个帐号映射到一个SQL Server数据库角色,然后为每个角色设置所需的数据库权限。
- 在数据访问层中使用应用程序逻辑确定执行数据库操作时,哪个Windows帐号需要冒充。
注意 每个帐号必须是同一域或信任域中在Internet信息服务(IIS)和SQL Server中存在的域帐号;也可以是在每台计算机上创建(具有相同用户名和密码)的匹配帐号。
为网络库使用TCP/IP
SQL Server 7.0及其以后版本支持用于所有网络库的Windows认证。使用TCP/IP可以获得配置、性能及扩展性优点。关于使用TCP/IP的更多信息,见本文通过防火墙建立链接 一节。
存储链接字符串
有多种方法可存储链接字符串,每种方法具有不同程度的灵活性和安全性。尽管在源代码中对字符串进行硬编码提供了最优性能,但文件系统缓存确保了与在文凭系统外部存储字符串相关的性能损失可被忽略。实际上外部链接字符串(允许管理员进行配置)所提供的附加灵活性在任何情况下都是受欢迎的。
选择存储链接字符串的方法时,首先要考虑的两个重要因素是配置的安全性与简易性,其次是性能。
可以选择将数据库链接字符串存储在下列位置:
- 应用程序配置文件 例如用于ASP.NET Web应用程序的Web.config文件。
- 通用数据链接文件(UDL) (只被OLE DB .NET 数据供应器所支持)
- Windows 注册表
- 定制文件
- COM+ 目录,通过过使用构造字符串(只用于服务组件)
使用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下面。
优点
- 易于部署。通过常规.NET xcopy部署,链接字符串随配置文件一起被部署。
- 通过程序易于访问。ConfigurationSettings类的AppSettings属性使得在运行时读取数据库链接字符串更为简单。
- 支持动态更新(仅限于ASP.NET)。如果管理员更新了Web.config文件中的链接字符串,那么下次在字符串被访问时所作出的变化生效,这对一个无状态的组件来说,就象客户再次利用组件作出了数据访问请求一样。
缺点
安全性。尽管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文件进行链接字符串的管理了。
缺点
- 性能。每次打开链接时,包含UDLs的链接字符串都被读取并被解析。
- 安全性。UDL文件以纯文本格式存储。利用NFTS文件权限可以确保这些文件的安全性,但这样做将引发与使用.config文件相同的问题。
- SqlClient对象不支持UDL文件。此方法不被 SQL Server .NET数据供应器所支持,而你要用此供应器访问 SQL Server 7.0及其以后版本。
更多信息
- 必须确保管理员拥有该文件的读/写访问权限以便进行管理,并且还要确保运行应用程序的身份拥有读权限。对于ASP.NET Web应用程序,应用程序工作者进程默认是以SYSTEM帐号运行的,但利用机器范围的配置文件(Machine.config)中的 元素可以将其覆盖掉。利用Web.config文件中的 元素,及一个可选的指定帐号,可以进行冒充。
- 对于Web应用程序,要确保没有将UDL文件放在虚目录中,因为那样会使该文件可通过网络下载。
- 关于这些及其它与安全性相关的ASP.NET特性的更多信息,见http://msdn.microsoft.com/library/en-us/dnbda/html/authaspdotnet.asp.。
使用Windows注册表
可以利用定制关键字将链接字符串存储在Windows注册表中,但由于部署问题,建议不要使用。
优点
- 安全性。利用访问控制列表(ACLs),可以对所选的注册表关键字的访问进行管理。对更高级别的安全性,考虑对数据进行加密。
- 通过程序易于访问。.NET类支持从注册表中读取字符串。
缺点
- 部署。相关的注册表设置必须同应用程序一起部署,从某种程度上抵消了xcopy部署的优点。
使用定置文件
可以使用定制文件来存储链接字符串,然而这种技术没有优点,因此并不推荐使用。
优点
- 没有
缺点
- 额外编码。这种方法需要额外编码,并迫使你明确处理同时发生的问题。
- 部署。此文件必须同其它ASP.NET应用程序文件一起拷贝。避免将此文件放在ASP.NET应用程序的目录或子目录中,就可以阻止通过网络对其进行下载。
使用构建参数和COM+目录
可以将链接字符串存储在COM+目录中,并利用对象的构造字符串将它自动地传递给对象。COM+在初始化对象,提供配置构造字符串后,将立即调用对象的Construct方法。
注意这个方法只用于服务组件。只有管理组件使用了其它服务,如分布式事务处理支持或对象池化时,才考虑使用此方法。
优点
- 管理性。利用组件服务MMC插件,管理员可以很方便地配置链接字符串。
缺点
- 安全性。COM+目录被认为是一个不安全的存储区(虽然利用COM+角色你可以限制对它的访问),并因此不能用于以明文维护链接字符串。
- 部署。COM+目录中的条目必须随.NET应用程序一同部署。如果使用了其它企业服务,如分布式事务或对象池化,那么将数据库链接字符串存储在目录中不会增加部署的额外开销,因为要支持其它服务,必须部署COM+目录。
- 必须为组件提供服务。可以只为所服务的组件使用构造字符串。要使能构造字符串,不能简单地从ServicedComponent类中派生所需组件类(这将为组件提供服务)。
更多信息
- 关于如何为对象构造配置.NET类的更多信息,见附录中的如何为.NET类使能对象构造 。
- 关于开发服务组件的更多信息,见http://msdn.microsoft.com/library/en-us/cpguide/html/cpconwritingservicedcomponents.asp。
链接使用方式
不管何种.NET数据供应器,你必须总是:
- 尽可能晚地打开数据库链接。
- 以尽可能短的时间使用该链接。
- 尽可能快地关闭该链接。链接直到通过Close或Dispose方法关闭后,它才返回到池中。即使发现它处于崩溃状态,也应当关闭它。这样做确保了它能返回池中,并被标记为无效。对象池周期性地扫描池,以查找已被标记为无效的对象。
为确保在方法返回前链接已经关闭,考虑使用下面两个代码片段中演示的方法。第一个示例使用了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 类所暴露的属性包含了例外状态的细节。其中包括:
- Message属性,它包含了用于描述错误的文本。
- Number属性,它包含唯一标识错误类型的错误号。
- State属性。它包含了关于错误启用状态的附加信息。它经常用于指示特殊错误状态的某个特定事件。例如,如果单一存储过程从不止一行中生成同样的错误,那么本属性将用于标识某个具体的事件。
- Errors集合。它包含了SQL Server生成的错误的详细信息。此集合部是包含至少一个SqlError类型的对象。
下面的代码片段演示了如何利用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将例外传回调用者。
更多信息
- 关于SqlException类成员的完整列表,见http://msdn.microsoft.com/library/en-us/cpref/html/frlrfSystemDataSqlClientSqlExceptionMembersTopic.asp。
- 关于定置例外的开发,.NET例外的记录与封装,返回例外的不同方法的使用的更多信息,见http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/exceptdotnet.asp。
从存储过程中生成错误
T-SQL提供了一个RAISERROR(注意拼写)函数。你可用此函数生成定置错误,并将错误返回客户。对于ADO.NET客户,SQL Server .NET数据供应器对这些数据错误进行解释,并把它们转化为SqlError对象。
使用RAISERROR函数是简单地方法是将消息文本作为第一个参数包括进来,然后指定严重及状态参数,如下面的代码片段所示:
RAISERROR( 'Unknown Product ID: %s', 16, 1, @ProductID )
在这个例子中,替代参数用于将当前产品ID作为错误消息文本的一部分返回,参数2是消息的严重性,参数3是消息状态。
更多信息
- 为了避免对消息文本进行硬编码,你可以利用sp_addmessage系统存储过程或SQL Server 企业管理器将你自己的消息增加到sysmessages表中。然后你就可以使用传递到RAISERROR函数的ID引用消息了。你所定义的消息Ids必须大于50000,如下代码片段所示:
- RAISERROR( 50001, 16, 1, @ProductID )
- 关于RAISERROR函数的完整细节,请在SQL Server的在线书目中查询RAISERROR。
正确使用严重性等级
仔细选择错误严重性等级,并要清楚每个级别造成的冲击。错误严重性等级的范围是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对象的抛出。
要获得通知型消息:
- >创建事件处理程序,并提交给SqlConnection对象所暴露的InfoMessage事件。下面的代码片段显示了事件代理。
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 多行数据访问方案方法比较
当从数据源中获取多行时,你可以使用下面的方法:
- 使用SqlDataAdapter对象生成DataSet 或 DataTabl对象。
- 利用SqlDataReader对象提供只读的只向前的数据流。
- 利用XmlReader对象提供只读的只向前的XML数据流。
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对象,当:
- 你需要非链接的驻留内存的缓存数据,以便你能将它传递到其它组件或应用程序中的其它层。
- 你需要内存中的数据关系图以执行XML或非XML操作。
- 你正在使用的数据来自多个数据源,如多个数据库、表或文件。
- 你希望更新获得的一些或所有行,并希望利用SqlDataAdapter的批更新功能。
- 你要对控件绑定数据,而此控件需要支持IList接口的数据源。
更多信息
如果使用SqlDataAdapter生成DataSet 或 DataTable,需注意:
- 不必明确打开或关闭数据库链接。SqlDataAdapter Fill方法打开数据库链接,并在此方法返回前关闭该链接。如果链接原来已经打开,那么此方法仍使链接处于打开状态。
- 如果出于其它目的需要链接,那么考虑在调用Fill方法前打开链接。这样你就可以避免不必要的打开/关闭操作,提高性能。
- 尽管能重复使用同一SqlCommand对象多执行同样的命令,但不要重复使用此对象执行不同的命令。
- 关于如何利用SqlDataAdapter对象填充DataSet 或 DataTable对象的代码示例,见附录中的如何利用SqlDataAdapter 对象获得多行。
使用SqlDataReader
些劣情况,可以使用通过调用 SqlCommand 对象的ExecuteReader方法得到的SqlDataReader对象:
- 正在处理大量数据时--太多了而不能在单个缓冲区内维护。
- 希望减少应用程序在内存中的印迹。
- 希望避免与DataSet对象创建相关的开销。
- 希望对某控件执行数据绑定操作,而此控件支持实现了IEnumerable接口的数据源。
- 希望流水线化数据访问,并对其优化。
- 正在读取包含二进制大对象(BLOB)列的行。你可以使用SqlDataReader对象以可管理的大块为单位从数据库中将BLOB数据拉出来,而不是一次性地将所有数据提取出来。关于处理BLOB数据的更多细节,见本文处理BLOBs 一节。
更多信息
如果使用SqlDataReader对象,请注意:
- 在数据阅读器活动期间,底层的数据库链接保持打开,并不能用于其它任何目的。尽可能早地对SqlDataReader对象调用Close方法。
- 每个链接只能有一个数据阅读器。
- 通过向ExecuteReader方法传递CommandBehavior.CloseConnection枚举值,可以在使用完数据阅读器后,明确地关闭链接;或者,将链接生命周期绑定到SqlDataReader对象。这预示着当SqlDataReader对象关闭时,链接也将关闭。
- 在利用阅读器访问数据时,如果你知道列的底层数据类型,那么就应使用类型化存取器方法(如GetInt32 和 GetString),这是因为在读取列数据时,这些方法减少了读取列数据所需的类型转换量。
- 为避免将不必要的数据从服务器发送到客户端,如果你要关闭阅读器并抛弃所有保留的结果,那么在对阅读器调用Close方法前调用命令对象的Cancel方法。Cancel方法确保了服务器的结果被抛弃,而不会被发送到客户端。相反,对数据阅读器调用Close方法会使阅读器不必要地提取出保留的结果,以清空数据流。
- 如果要得到从存储过程返回的输出值或返回值,并且你在利用SqlCommand对象的ExecuteReader方法,那么在得到输出或返回值前,必须对阅读器调用Close方法。
- 关于演示如何利用SqlDataReader对象的代码示例,附录中的如何利用SqlDataReader对象获取多行数据。
使用XmlReader
下列情况下,使用通过调用SqlCommand对象的ExecuteXmlReader方法得到的XmlReader对象:
- 希望将得到的数据作为XML 处理,但不希望引发因创建DataSet对象而造成的额外性能开销,并且不需要数据的非链接缓存。
- 希望利用SQL Server FOR XML 语法的功能,这种语法允许以灵活的方式从数据库中得到XML片段(即,不带根元素的XML文档)。例如,这种方法使你能够精确指定元素名,是使用元素还是使用以属性为核心的图解,图解是否随XML数据一起被返回,等等。
更多信息
如果使用XmlReader,请注意:
- 在从XmlReader对象中读取数据时,链接必须保持打开。SqlCommand对象的 ExecuteXmlReader方法目前不支持CommandBehavior.CloseConnection枚举值,因此在使用完阅读器后必须明确关闭链接。
- 对于如何使用XmlReader对象的代码示例,见附录中的如何利用 XmlReader获取多行数据。
获取单行数据
在这种场景中,将从数据源中获取包含一组指定列的单行数据。例如,你得到一个客户ID,并希望查找与客户相关的细节;或得到一个产品ID,并希望得到产品信息。
方法比较
如果要对从数据源中得到的一行数据执行绑定操作,可以用SqlDataAdapter对象填充DataSet 或DataTable对象,其方式与在先前讨论过的获取多行数据及重复场景中描述的方式相同。然而,除非特别需要DataSet 或DataTable对象的功能,否则应当避免创建这些对象。
如果需要获取单行数据,那么请使用下面的一种方法:
这两种方法都避免了在服务器端创建结果集,在客户端创建DataSet对象的不必要额外开销。每种方法的相对性能要依赖于强度等级及数据库链接池化是否被使能。当数据库链接池化使能时,性能测试表明存储过程方法在高强度环境下(同时存在200多链接)其性能比SqlDataReader方法高近30%。
使用存储过程输出参数
如下情况中使用存储过程输出参数:
- 要从链接池化使能的多层Web应用程序中获得一行数据。
更多信息
- 关于演示如何使用存储过程输出参数的代码示例,见附录中的使用存储过程输出参数获取一行数据。
使用SqlDataReader对象
下列情况,需使用SqlDataReader对象:
- 除了数据值,还需要元数据时。可以利用数据阅读器的GetSchemaTable方法获取列元数据。
- 未使用链接池化时。在链接池化无效时,SqlDataReader对象在所有强度环境下都是好方式;性能测试表明,在200浏览器链接时,此方法比存储过程方法在性能上要高约20%。
更多信息
- 如果知道查询结果只需返回一行,那么在调用SqlCommand对象的ExecuteReader 方法时,使用CommandBehavior.SingleRow枚举值。一些供应器,如OLE DB .NET数据供应器,用此技巧来优化性能。例如,供应器使用IRow接口(如果此接口存在)而不是代价更高的IRowset接口。这个参数对SQL Server .NET数据供应器没有影响。
- 在使用SqlDataReader对象时,总是应当通过SqlDataReader对象的类型化存取器方法,如GetString 和GetDecimal,获得输出参数。这样做就避免了不必要的类型转换。
- 关于如何使用SqlDataReader对象获取单行数据的代码示例,见附录中的如何使用 SqlDataReader对象获取单行数据。
获取单项数据
在本场景中,要获取单项数据。例如,提供了产品ID后,希望查询单一的产品名;或,给出了客户名后,希望查询客户的信用等级。在这种场景中,为得到单项数据,通常不希望引发创建DataSet 对象或甚至是 DataTable对象的额外开销。
也许只希望检查数据库中是否存在特定的行。例如,当新用户在网站注册时,需要检查所选用户名是否已经存在。这是单项数据查询中很特殊的例子,但在此例子中,返回一个简单的布尔返回值就足够了。
方法比较
当从数据源获取单项数据时,考虑下面的方法:
- 同存储过程一起使用SqlCommand对象的ExecuteScalar方法。
- 使用存储过程输出或返回参数。
- 使用SqlDataReader对象。
ExecuteScalar方法直接返回数据项,因为它是为只返回单个值的查询设计的,与存储过程输出参数和SqlDataReader方法相比,它需要更少的代码。
从性能方面来说,应当使用存储过程输出或返回参数,因为测试结果表明,存储过程方法在从低强度到高强度环境中(从同时不到100浏览器链接到200浏览器链接)提供了一致的性能。
更多信息
- 如果通过ExecuteQuery方法所执行的查询返回多列和/或行,那么此方法只返回第一行的第一列。
- 关于演示如何使用ExecuteScalar方法的代码片段,见附录中的如何使用 ExecuteScalar获取单项数据。
- 关于演示如何利用存储过程输出或返回参数获取单项数据的代码示例,见附录中的如何利用存储过程输出或返回参数获取单项数据
- 关于演示如何使用SqlDataReader对象获取单项数据的代码示例,见附录中的如何使用 SqlDataReader对象获取单项数据。
需要经常配置互联网应用程序以使它能够通过防火墙链接到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服务器)。在这种情况下,可以按如下方式之一解决:
- 利用通过链接字符串提供的“Network Library=dbmssocn”名称-值对指定网络库。字符串dbmssocn用于标识TCP/IP(套接字)库。
注意 在使用SQL Server .NET数据供应器时,网络库的默认设置是使用“dbmssocn”。
- 在客户端机器上修改注册表,把TCP/IP设置为默认库。关于配置SQL Server网络库的更多信息,参见HOWTO: 不使用客户端网络应用程序而修改SQL Server默认网络库(Q250550)。
指定端口
如果SQL Server的实例被配置为监听默认的1433以外的其它端口,那么通过以下操作,就能指定链接到的端口号:
- 使用客户端网络应用程序
- 利用提供给链接字符串的“Server”或“Data Source”名称-值对来指定端口号。要按下面的格式使用字符串:
- "Data Source=ServerName,PortNumber"
注意 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数据与行中其它项数据的同步。
- BLOB数据由数据库所支持,拥有单一的存储流,易于管理。
- 通过SQL Server 2000所支持的XML可以访问BLOB数据,这将在XML流中返回64位编码描述的数据。
- 对包含了固定或可变长度的字符(包括宽字符)数据的列可以执行SQL Server全文本搜索(FTS)操作。也可以对包含在image字段中的已格式化的基于文本的数据--Word 或 Excel文档--执行FTS操作。
将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。
可以采用很多方法将事务管理合并到数据访问代码中。每种方法适合下面两种基本编程模型之一。
- 手工事务处理。可以直接在组件代码或存储过程中分别编写利用ADO.NET 或 Transact-SQL事务处理支持特性的代码。
- 自动化(COM+)事务处理。可以向.NET类中增加声明在运行时指定对象事务处理需要的属性。这种模型使你能方便地配置多个组件以使它们在同一事务处理内运行。
尽管自动化事务处理模型极大地简化了分布式事务处理过程,但两种模型都用于执行本地事务处理(即对单个资源管理器如SQL Server 2000执行的事务处理)或分布式事务处理(即,对位于远程计算机上的多个资源管理执行的事务处理)。
你也许会试图利用自动化(COM+)事务处理来从易于编程的模型中获益。在有多个组件执行数据库更新的系统中,这种优点更明显。然而,在很多情况下,应当避免这种事务处理模型所带来的额外开销和性能损失。
本节将指导你根据特定的应用程序环境选择最合适的模型。
选择事务处理模型
在选择事务处理模型前,首先应当考虑是否真正需要事务处理。事务处理是服务器应用程序使用的最昂贵的资源,在不必要使用的地方,它们降低了扩展性。考虑下面用于管理事务处理使用的准则:
- 只在需要跨一组操作获取锁并需要加强ACID规则时才执行事务处理。
- 尽可能短地保持事务处理,以最小化维持数据库锁的时间。
- 永远不要将客户放到事务处理生命周期的控制之中。
- 不要为单个SQL语句使用事务处理。SQL Server自动把每个语句作为单个事务处理执行。
自动化事务处理与手工事务处理的对比
尽管编程模型已经对自动化事务处理进行了简化,特别是在多个组件执行数据库更新时,但本地事务处理总是相当快,因为它们不需要与微软DTC交互。即使你对单个本地资源管理器(如SQL Server)使用自动化事务处理,也是这种情况(尽管性能损失减少了),因为手式本地事务处理避免了所有不必要的与DTC的进程间通信。
对于下面的情况,需使用手工事务处理:
- 对单个数据库执行事务处理。
对于下列情况,则宜使用自动事务处理:
- 需要将单个事务处理扩展到多个远程数据库时。
- 需要单个事务处理拥有多个资源管理器(如数据库和Windows 2000消息队列(被称为MSMQ)资源管理器)时。
注意 避免混用事务处理模型。最好只使用其中一个。
在性能足够好的应用程序环境中,(甚至对于单个数据库)选择自动化事务处理以简化编程模型,这种做法是合理的。自动化事务处理使多个组件能很容易地执行现一事务处理中的多个操作。
使用手工事务处理
对于手工事务处理,可以直接在组件代码或存储过程中分别编写使用ADO.NET 或 Transact-SQL事务处理支持特性的代码。多数情况下,应选择在存储过程中控制事务处理,因为这种方法提供了更高的封装性,并且在性能方面,此方法与利用ADO.NET 代码执行事务处理兼容。
利用ADO.NET执行手工事务处理
ADO.NET支持事务处理对象,利用此对象可以开始新事务处理过程,并明确控制事务处理是否执行还是回滚。事务处理对象与单个数据库链接相关,可以通过链接对象的BeginTransaction方法获得。调用此方法并不是暗示,接下来的命令是在事务处理上下文中发出的。必须通过设置命令的Transaction属性,明确地将每个命令与事务处理关联起来。可以将多个命令对象与事务处理对象关联,因此在单个事务处理中就针对单个数据库把多个操作进行分组。
关于使用ADO.NET事务处理代码的示例,见附录中如何编码ADO.NET手工事务处理。
更多信息
- ADO.NET手工事务处理的默认分离级别是读联锁,这意味着在读取数据时,数据库控制共享锁,但在事务处理结束前,数据可以被修改。这种情况潜在地会产生不可重复的读取或虚数据。通过将事务处理对象的IsolationLevel属性设置为IsolationLevel枚举类型所定义的一个枚举值,就可改变分离级别。
- 必须仔细为事务处理选择合适的分离级别。其折衷是数据一致性与性能的比例。最高的分离等级(被序列化了)提供了绝对的数据一致性,但是以系统整体吞吐量为代价。较低的分离等级会使应用程序更易于扩展,但同时增加了因数据不一致而导致出错的可能性。对多数时间读取数据、极少写入数据的系统来说,较低的分离等级是合适的。
- 关于选择恰当事务处理级别极有价值的信息,见微软出版社名为Inside SQL Server 2000的书,作者Kalen Delaney。
利用存储过程执行手工事务处理
也可以在存储过程中使用Transact-SQL语句直接控制手工事务处理。例如,可以利用包含了Transact-SQL事务处理语句(如BEGIN TRANSACTION、END TRANSACTION及ROLLBACK TRANSACTION)的存储过程执行事务处理。
更多信息
- 如果需要,可以在存储过程中使用SET TRANSACTION ISOLATION LEVEL语句控制事务处理的分离等级。读联锁是SQL Server的默认设置。关于SQL Server分离级别的更多信息,见SQL Server在线书目“访问和修改关系数据”一节中的分离级别部分。
- 关于演示如何利用Transact-SQL事务处理语句执行事务更新的代码示例,见附录中的如何利用Transact-SQL执行事务处理。
使用自动化事务
自动化事务简化了编程模型,因为它们不需要明确地开始新事务处理过程,或明确执行或取消事务。然而,自动化事务的最大优点是它们能与DTC结合起来,这就使单个事务可以扩展到多个分布式数据源中。在大型分布式应用程序中,这个优点是很重要的。尽管通过手工对DTC直接编程来控制分布式事务是可能的,但自动化事务处理极大的简化了工作量,并且它是为基于组件的系统而设计的。例如,可以方便地以说明方式配置多个组件以执行包含了单个事务处理的任务。
自动化事务依赖于COM+提供的分布式事务处理支持特性。结果,只有服务组件(即从ServicedComponent类中派生的组件)能够使用自动化事务。
要为自动化事务处理配置类,操作如下:
- 从位于EnterpriseServices名称空间的ServicedComponent类中派生新类。
- 通过Transaction属性定义类的事务处理需求。来自TransactionOption的枚举值决定了如何在COM+类中配置类。可与此属性一同设置的其它属性包括事务处理分离等级和超时上限。
- 为了避免必须明确选出事务处理结果,可以用AutoComplete属性对方法进行注释。如果这些方法释放异常,事务将自动取消。注意,如果需要,仍可以直接挑选事务处理结果。更多详情,见本文稍后确定事务处理结果的节。
更多信息
- 关于COM+自动化事务的更多信息,可在平台SDK文档中搜索“通过COM+的自动化事务”获取。
- 关于.NE T事务处理类的示例,见附录中的如何编码.NET事务处理。
配置事务处理分离级别
用于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对象中控制事务处理结果:
- 可以用AutoComplete属性对方法进行注释,并让.NET自动存放将决定事务处理结果投票。如果方法释放异常,利用此属性,一致性标志自动地被设为假(此值最终使事务取消)。如果方法返回而没有释放异常,那么一致性标志将设为真,此值指出组件乐于执行事务。这并没有得到保证,因为它依赖于同一事务流中其它对象的投票。
- 可以调用ContextUtil类的静态方法SetComplete或 SetAbort,这些方法分别将一致性标志设为真或假。
严重性大于10的SQL Server错误将导致管理数据供应器释放SqlException类型的异常。如果方法缓存并处理异常,就要确保或者通过手工取消了事务,或者方法被标记了[AutoComplete],以保证异常能传递回调用者。
AutoComplete方法
对于标记了属性的方法,执行下面操作:
- 将SqlException传递加调用堆栈。
- 将SqlException封装在外部例外中,并传递回调用者。也可以将异常封装在对调用者更有意义的异常类型中。
异常如果不能传递,将导致对象不会提出取消事务,从而忽视数据库错误。这意味着共享同一事务流的其它对象的成功操作将被提交。
下面的代码缓存了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的属性的方法,必须:
- 在catch块内调用ContextUtil.SetAbort以终止事务处理。这就将相容标志设置为假。
- 如果没有发生异常事件,调用ContextUtil.SetComplete,以提交事务,这就将相容标志设置为真(缺省状态)。
代码说明了这种方法。
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的Fill方法,将来自查询处的结果填充到DataSet中。
- 通过COM的可相互操作性使用ADO,并利用服务器光标。
- 利用存储的过程手工实现数据分页。
对数据进行分页的最优选项依赖于下列因素:
- 扩展性要求
- 性能要求
- 网络带宽
- 数据库服务器的存储器和功率
- 中级服务器的存储器和功率
- 由分页查询所返回的行数
- 数据总页数的大小
性能测试表明利用存储过程的手工方法在很大的应力水平范围上都提供了最佳性能。然而,由于手工方法在服务器上执行工作,如果大部分站点功能都依赖数据分页功能,那么服务器性能就会成一个关键要素。为确保这种方法能适合特殊环境,应该测试各种特殊要求的选项。
下面将讨论各种不同的选项。
使用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那样),就可以使用服务器光标。这样就可以利用光标直接导航到起始记录,而不需要将所有数据传过网络进入访问数据的用户代码中。
这种方法有下面两个缺点:
- 在大多数情况下,可能需要将返回到Recordset对象中的记录翻译成DataSet中的内容,以便在客户管理的代码中使用。虽然OleDbDataAdapter确实在获取ADO Recordset对象并把它翻译成Dataset时过载了Fill方法,但是并没有利用特殊记录进行开始与结束操作的功能。唯一现实的选项是把开始记录移动到Recordset对象中,循环每个记录,然后手工拷贝数据到手工生成的新Dataset中。这种操作,尤其是利用COM Interop调用,其优点可能不仅仅是不需要在网络上传输多余的数据,尤其对于小的DataSet更明显。
- 从服务器输出所需数据时,将保持连接和服务器光标开放。在数据库服务器上,光标的开放与维护需要昂贵的资源。虽然该选项提高了性能,但是由于为延长的时间两消耗服务器资源,从而也有可能降低可扩展性。
提供手工实现
在本部分中讨论的数据分页的最后一个选项是利用存储过程手工实现应用程序的分页功能。对于包含唯一关键字的表格,实现存储过程相对容易一些。而对于没有唯一关键字的表格(也不应该有许多关键字),该过程会相对复杂一些。
带有唯一关键字的表格的分页
如果表格包含一个唯一关键字,就可以利用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管理的类,需要执行下列步骤:
- 从位于System. Enterprise Services名字空间中的Serviced Component中导出所需类。
using System.EnterpriseServices; public class DataAccessComponent : ServicedComponent
- 为该类添加Construction Enabled属性,并合理地指定缺省结构字符串,该缺省值保存在COM+目录中,管理员可以利用组件服务微软管理控制台(MNC)的snap-in来维护该缺省值。
[ConstructionEnabled(Default="default DSN")] public class DataAccessComponent : ServicedComponent
- 提供虚拟Construct方法的替换实现方案。该方法在对象语言构造程序之后调用。在COM目录中保存的结构字符串是该方法的唯一字符串。
public override void Construct( string constructString ) { // Construct method is called next after constructor. // The configured DSN is supplied as the single argument }
- 通过Assembly key文件或Assembly key Name属性为该汇编提供一个强名字。任何用COM+服务注册的汇编必须有一个强名字。关于带有强名字汇编的更多信息,参考:http://msdn.microsoft.com/library/en-us/cpguide/html/cpconworkingwithstrongly- namedassemblies.Asp。
[assembly: AssemblyKeyFile("DataServices.snk")]
- 为支持动态注册,可以利用汇编层上的属性ApplicationName和Application Action分别指定用于保持汇编元素和应用程序动作类型的COM+应用程序的名字。关于汇编注册的更多信息,参考: http://msdn.microsoft.com/library/en-us/cpguide/html/cpconregisteringserviced components.asp。
// 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:
- 创建SqlCommand对象启用存储过程,并把它与SqlConnection对象(显示的)或连接字符串(未显示)相联系。
- 创建一个新的SqlDataAdapter对象,并把它SqlCommand对象相联系。
- 创建DataTable(或者DataSet)对象。利用构造程序自变量命名DataTable.
- 调用SqlData Adapter对象的Fill方法,把检索的行转移到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检索多行:
- 创建用于执行存储的过程的SqlCommand对象,并把它与SqlConnection对象相联系。
- 打开链接。
- 通过调用SqlCommand对象的Excute Reader方法生成SqlDataReader对象。
- 从流中读取数据,调用SqlDataReader对象的Read方法来检索行,并利用分类的存取程序方法(如GetIut 32和Get String方法)检索列的值。
- 完成读取后,调用Close方法。
如何利用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数据:
- 创建SqlCommand对象启用生成XML结果设置的过程。(比如,利用SELECT状态中的FOR XML条款)。把SqlCommand对象与一个链接相联系。
- 调用SqlCommand对象的ExecuteXmlReader方法,并把结果分配给前向对象XmlTextReader。当不需要任何返回数据的基于XML的验证时,这是应该使用的最快类型的XmlReader对象。
- 利用XmlTextReader对象的Read方法读取数据。
如何利用存储过程输出参数检索单个行
可以调用一个存储过程,它通过一种称做输出参数的方式可以在单个行中返回检索数据项。下列代码段利用存储的过程检索产品的名称和单价,该产品包含在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(); } }
按下列步骤利用存储的过程输出参数检索单个行:
- 创建一个SqlCommand对象,并把它与SqlConnection对象相联系。
- 通过调用SqlCommand’s Parameters集合的Add方法设置存储过程参数。缺省情况下,参数假定为输出参数,所以必须明确设置任何输出参数的方向。
注意 明确设置所有参数的方向是一次很好的练习,包括输入参数。
- 打开连接。
- 调用Sqlcommand对象的ExecuteNonQuery方法。它在输出参数(并潜在地带有一个返回值)中。
- 利用Value属性从合适的SqlParameter对象中检索输出参数。
- 关闭连接。
上述代码段启用了下列存储过程。
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对象:
- 建立SqlCommand对象。
- 打开连接。
- 调用SqlDReader对象的ExecuteReader对象。
- 利用SqlDataReader对象的分类的存取程序方法检索输出参数--在这里是GetString和GetDecimal.
上述代码段启用了下列存储过程:
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检索单个项:
- 建立调用存储过程的SqlCommand对象。
- 打开链接。
- 调用ExecuteScalar方法,注意该方法返回对象类型。它包含检索的第一列的值,并且必须设计成合适的类型。
- 关闭链接。
上述代码启用了下列存储过程:
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(); } }
按下列步骤利用存储过程的输出参数检索单个值:
- 创建调用存储过程的SqlCommand对象。
- 通过把SqlParmeters添加到SqlCommand’s Parameters集合中设置任何输入参数和单个输出参数。
- 打开链接。
- 调用SqlCommand对象的Execute NonQuery方法。
- 关闭链接。
- 利用输出SqlParameter的Value属性检索输出值。
上述代码使用了下列存储过程:
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; }
按下列步骤,可以利用存储过程返回值检查是否存在特殊行:
- 建立调用存储过程的SqlCommand对象。
- 设置包含需要访问的行的主要关键字的输入参数。
- 设置单个返回值参数。把SqlParameter对象添加到SqlCommand’s Parameter集合中,并设置它到ParameterDireetion.ReturnValue的方面。
- 打开链接。
- 调用SqlCommand对象的ExecuteNonQuery的方法.
- 关闭链接。
- 利用返回值SqlParameter的Value属性检索返回值。
上述代码使用了下列存储过程:
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。