数据绑定
所有这三个对象都可以作为数据绑定控件的数据源。而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。