概述:本文提供了在多层.net应用程序中实施基于ado.net的数据访问层的指导原则。其重点是一组通用数据访问任务和方案,并指导你选择最合适的途径和技术(68张打印页)。
目录
ado.net简介
管理数据库链接
错误处理
性能
通过防火墙建立链接
处理 blobs
事务处理
数据分页
简介
如果你在为.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简介
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数据供应器
ole db .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对象的功能,否则应当避免创建这些对象。
如果需要获取单行数据,那么请使用下面的一种方法:
使用存储过程输出参数.
使用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执行事务处理。注册会员,创建你的web开发资料库,