精华的微软文章.NET 数据访问架构指南

访问|架构|精华|数据|微软

数据绑定

所有这三个对象都可以作为数据绑定控件的数据源。而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对象的功能,否则应当避免创建这些对象。

如果需要获取单行数据,那么请使用下面的一种方法:

使用存储过程输出参数.
使用SqlDataReader对象.
这两种方法都避免了在服务器端创建结果集,在客户端创建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

目前,很多应用程序除了处理许多传统的字符串和数字型数据外,还要处理象图形或声音--甚至复杂的数据格式,如视频格式的数据。图形、声音与视频的数据格式类型不一。然而从存储角度来说,它们都可被视为二进制数据块,通常将其称为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至devfdbckmicrosoft.com。

你希望学习并利用.NET的强大功能吗?与微软技术中心的技术专家一起工作,学习开发最佳方案。详细信息请访问: http://www.micrsoft.com/business/services/mtc.asp。

时间: 2016-02-06

精华的微软文章.NET 数据访问架构指南的相关文章

.NET 数据访问架构指南(一)

访问|架构|数据 概述:本文提供了在多层.NET应用程序中实施基于ADO.NET的数据访问层的指导原则.其重点是一组通用数据访问任务和方案,并指导你选择最合适的途径和技术(68张打印页). 目录 ADO.NET简介 管理数据库链接 错误处理 性能 通过防火墙建立链接 处理 BLOBs 事务处理 数据分页 简介 如果你在为.NET应用程序设计数据访问层,那么就应该把 Microsoft ADO.NET用作数据访问模型.ADO.NET扩展丰富,并且支持结合松散的数据访问需求.多层Web应用程序及We

.NET 数据访问架构指南(二)

访问|架构|数据 使用自动化事务 自动化事务简化了编程模型,因为它们不需要明确地开始新事务处理过程,或明确执行或取消事务.然而,自动化事务的最大优点是它们能与DTC结合起来,这就使单个事务可以扩展到多个分布式数据源中.在大型分布式应用程序中,这个优点是很重要的.尽管通过手工对DTC直接编程来控制分布式事务是可能的,但自动化事务处理极大的简化了工作量,并且它是为基于组件的系统而设计的.例如,可以方便地以说明方式配置多个组件以执行包含了单个事务处理的任务. 自动化事务依赖于COM+提供的分布式事务处

.NET 数据访问体系结构指南一

访问|数据|体系  ADO.NET 引入的主要变化之一是用 DataTable.DataSet.DataAdapter 和 DataReader 对象的组合取代了 ADO Recordset 对象.DataTable 表示单个表中行的集合,在这一方面类似于 Recordset.DataSet 表示 DataTable 对象的集合,同时包括将各种表绑定在一起的关系和约束.实际上,DataSet 是带有内置 XML 支持的.内存中的关系结构. DataSet 的主要特性之一是它不了解可能用来填充它的

微软痛失数据中心架构师Michael Manos

据悉,微软公司的数据中心架构师Michael Manos近日被硅谷数字装备公司(Silicon Valley outfit Digital Realty Trust)挖走. 不过,微软方面还未宣布关于Manos离职的任何官方评论及答复. Manos将在新公司负责负责全球数据中心的设计.操作.搜索及架构.此外,他还将为新的公司启动一项专业的服务型软件方案. Manos是在五年前加入微软的.在2008年4月他被提升为数据中心软件部门的总经理,同月微软在芝加哥搭设了自己的可容纳150-200个集装箱的

.NET轻量级DBHelpers数据访问组件

一.摘要 一说到ADO.NET大家可能立刻想到的就是增.删.改.查(CRUD)操作,然后再接就想到项目中的SQLHelper.没错本课分享课阿笨给大家带来的是来源于github上开源的DAO数据库访问组件DBHelpers.如果您对本次分享<.NET轻量级DBHelpers数据访问组件>课程感兴趣的话,那么请跟着阿笨一起学习吧. 废话不多说,直接上干货,我们不生产干货,我们只是干货的搬运工. 二.涉及覆盖的知识点 2.1.原生ADO.NET简单的CRUD(增删改查) Insert.Insert

基于.NET平台的分层架构实战(九)—数据访问层的第三种实现:基于NBear框架

基于.NET平台的分层架构实战(九)-数据访问层的第三种实现:基于NBear框架的ORM实现 前面的文章讨论了使用SQL语句和存储过程两种数据访问层的实现方式,这一篇里,将讨论使用ORM方式实现数据访问层的方法. 对象-关系映射(Object/Relation Mapping,简称ORM),是随着面向对象的软件开发方法发展而产生的.面向对象的开发方法是当今企业级应用开发环境中的主流开发方法,关系数据库是企业级应用环境中永久存放数据的主流数据存储系统.对象和关系数据是业务实体的两种表现形式,业务实

[原创].NET 分布式架构开发实战之三 数据访问深入一点的思考

原文:[原创].NET 分布式架构开发实战之三 数据访问深入一点的思考 .NET 分布式架构开发实战之三 数据访问深入一点的思考   前言:首先,感谢园子里的朋友对文章的支持,感谢大家,希望本系列的文章能够真正的对大家起到一点帮助的作用.再次感谢大家.   大家也许想问,什么时候出代码,代码一定会出的,我不想一上来就开始抛出一大堆的代码,然后讲解,架构的设计在思考的过程,思考到了,代码也就水到渠成了.   上篇文章讲述在设计之初,Richard所画出的一些草图,本篇对之前的草图做了进一步的思考.

.NET 分布式架构开发“.NET研究”实战之三 数据访问深入一点的思考

前言: 首先,感谢朋友们对文章的支持,感谢大家,希望本系列的文章能够真正的对大家起到一点帮助的作用.再次感谢大家. 大家也许想问,什么时候出代码,代码一定会出的,我不想一上来就开始抛出一大堆的代码,然后讲解,架构的设计在思考的过程,思考到了,代码也就水到渠成了. 上篇文章讲述在设计之初,Richard所画出的一些草图,本篇对之前的草图做了进一步的思考. 本篇的议题如下: 1.草图的一些问题在哪里 2.重审之前项目中数据层的问上海闵行企业网站设计与制作题 3.思维的一点突破 4.回首再看数据访问层

一起谈.NET技术,.NET 分布式架构开发实战之三 数据访问深入一点的思考

前言: 首先,感谢朋友们对文章的支持,感谢大家,希望本系列的文章能够真正的对大家起到一点帮助的作用.再次感谢大家. 大家也许想问,什么时候出代码,代码一定会出的,我不想一上来就开始抛出一大堆的代码,然后讲解,架构的设计在思考的过程,思考到了,代码也就水到渠成了. 上篇文章讲述在设计之初,Richard所画出的一些草图,本篇对之前的草图做了进一步的思考. 本篇的议题如下: 1.草图的一些问题在哪里 2.重审之前项目中数据层的问题 3.思维的一点突破 4.回首再看数据访问层 1.草图的一些问题在哪里