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

本文提供了在多层.NET应用程序中实施基于ADO.NET的数据访问层的指导原则......

概述:本文提供了在多层.NET应用程序中实施基于ADO.NET的数据访问层的指导原则。其重点是一组通用数据访问任务和方案,并指导你选择最合适的途径和技术(68张打印页)。

  简介

  如果你在为.NET应用程序设计数据访问层,那么就应该把 Microsoft ADO.NET用作数据访问模型。ADO.NET扩展丰富,并且支持结合松散的数据访问需求、多层Web应用程序及Web服务。通常,它利用许多扩展丰富的对象模型, ADO.NET提供了多种方法用于解决一个特定问题。

  本文将指导你选择最合适的数据访问方法,其做法是详细列出大范围的通用数据访问方案,提供运用技巧,并且建议最优实践。本文还回答了其它经常问到的问题:何处最适合存放数据库链接字符串?应如何实现链接存储池?如何处理事务?如何实现分页以允许用户在许多记录中滚动?

  注意本文的重点是ADO.NET的使用:利用SQL Server .NETData Provider--随ADO.NET一起提供的两个供应器之一--访问Microsoft SQL Server 2000。本文在合适的地方,将突出显示在你使用OLE DB .NET数据供应器访问其它OLE DB敏感数据源时需要注意的所有差别。

  对于利用本文所讨论的指导原则和最优实践所开发的数据访问组件的具体实现,见(Data Access Application Block)数据访问应用程序块。注意,本实现的源代码是可以获得的,并且能直接用于你的.NET应用程序中。

  谁应当阅读本文?

  本文为希望构建.NET应用程序的应用程序设计师和企业开发人员提供了指导原则。如果你负责设计并开发多层.NET应用程序的数据层,那么请阅读本文。

  你首先需要知道什么?

  要利用本指南构建.NET应用程序,你必须有利用ActiveX数据对象(ADO)和/或 OLE DB开发数据访问代码的实际经验,及SQL Server经验。你也必须明白如何为.NET平台开发管理代码,并且也必须清楚ADO.NET数据访问模型引入的基本变化。

  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对象的所有实现都必须支持这些接口。

  图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链接池经常被称为会话或资源池。

  ADO.NET数据供应器提供了透明的链接池,每种链接池的确切机制对每种供应器来说是不同的。本节讨论的链接池是关于:
  • SQL Server .NET数据供应器

  • OLE DB .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链接池经常被称为会话或资源池。

  ADO.NET数据供应器提供了透明的链接池,每种链接池的确切机制对每种供应器来说是不同的。本节讨论的链接池是关于:
  • SQL Server .NET数据供应器

  • OLE DB .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链接池经常被称为会话或资源池。

  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)以帮助查找链接泄漏。

  如果设立了最小池容量,那么当池最初被填充以达到该值时,会导致一些性能损失,尽管最初链接的几个客户会从中受益。注意,创建新链接的过程被序列化了,这就意味着当池最初被填充时,服务器无法处理同时发生的请求。

  更多信息

  在使用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 监视链接池化,操作如下:
  1. 单击开始,指向程序,指向Microsoft SQL Server,然后单击Profiler运行Profiler。

  2. 文件菜单中,指向新建,然后单击跟踪

  3. 提供链接内容,然后单击确定

  4. 跟踪属性对话框中,单击事件标签。

  5. 已选事件类别列表中,确保审核登录审核登出事件显示在安全审核下面。

  6. 单击运行开始跟踪。在链接建立时,将会看到审核登录事件;在链接关闭时看到审核登出事件。
  要通过性能监视器监视链接池化,操作如下:
  1. 单击开始,指向程序,指向管理工具,然后单击性能运行性能监视器。

  2. 在图表背景中右击,然后单击增加计数器。

  3. 在性能对象下拉列表框中,单击SQL Server:通用统计。

  4. 在出现的列表中,单击用户链接。

  5. 单击增加,然后单击关闭。
  注意 .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将数据库链接字符串存储在应用程序配置文件的定制设置部分。该元素支持任意关键字-值对,如下面的代码片段所示:

value="server=(local);Integrated Security=SSPI;database=northwind"/>

  注意: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"];
}

使用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文件放在虚目录中,因为那样会使该文件可通过网络下载。

  使用Windows注册表

  可以利用定制关键字将链接字符串存储在Windows注册表中,但由于部署问题,建议不要使用。

  优点
  • 安全性。利用访问控制列表(ACLs),可以对所选的注册表关键字的访问进行管理。对更高级别的安全性,考虑对数据进行加密。

  • 通过程序易于访问。.NET类支持从注册表中读取字符串。
  缺点
  • 部署。相关的注册表设置必须同应用程序一起部署,从某种程度上抵消了xcopy部署的优点。
  使用定置文件

  可以使用定制文件来存储链接字符串,然而这种技术没有优点,因此并不推荐使用。

  优点
  • 没有
  缺点
  • 额外编码。这种方法需要额外编码,并迫使你明确处理同时发生的问题。

  • 部署。此文件必须同其它ASP.NET应用程序文件一起拷贝。避免将此文件放在ASP.NET应用程序的目录或子目录中,就可以阻止通过网络对其进行下载。
  使用构建参数和COM+目录

  可以将链接字符串存储在COM+目录中,并利用对象的构造字符串将它自动地传递给对象。COM+在初始化对象,提供配置构造字符串后,将立即调用对象的Construct方法。

  注意这个方法只用于服务组件。只有管理组件使用了其它服务,如分布式事务处理支持或对象池化时,才考虑使用此方法。

  优点
  • 管理性。利用组件服务MMC插件,管理员可以很方便地配置链接字符串。

    缺点
  • 安全性。COM+目录被认为是一个不安全的存储区(虽然利用COM+角色你可以限制对它的访问),并因此不能用于以明文维护链接字符串。

  • 部署。COM+目录中的条目必须随.NET应用程序一同部署。如果使用了其它企业服务,如分布式事务或对象池化,那么将数据库链接字符串存储在目录中不会增加部署的额外开销,因为要支持其它服务,必须部署COM+目录。

  • 必须为组件提供服务。可以只为所服务的组件使用构造字符串。要使能构造字符串,不能简单地从ServicedComponent类中派生所需组件类(这将为组件提供服务)。
  链接使用方式

  不管何种.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将例外传回调用者。

从存储过程中生成错误

  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及其以下 NoNo通知型消息,并不表示犯错误状态。
11-16NoYes可由用户修改的错误,例如,使用修改后的输入数据重试操作。
17-19NoYes资源或系统错误。
20-25YesYes致命的系统错误(包括硬件错误)。客户链接被终止。

  控制自动化事务

  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对象多执行同样的命令,但不要重复使用此对象执行不同的命令。
  使用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方法。
  使用XmlReader

  下列情况下,使用通过调用SqlCommand对象的ExecuteXmlReader方法得到的XmlReader对象:
  • 希望将得到的数据作为XML 处理,但不希望引发因创建DataSet对象而造成的额外性能开销,并且不需要数据的非链接缓存。

  • 希望利用SQL Server FOR XML 语法的功能,这种语法允许以灵活的方式从数据库中得到XML片段(即,不带根元素的XML文档)。例如,这种方法使你能够精确指定元素名,是使用元素还是使用以属性为核心的图解,图解是否随XML数据一起被返回,等等。
  更多信息

  如果使用XmlReader,请注意:
  • 在从XmlReader对象中读取数据时,链接必须保持打开。SqlCommand对象的 ExecuteXmlReader方法目前不支持CommandBehavior.CloseConnection枚举值,因此在使用完阅读器后必须明确关闭链接。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值