剖析 .NET 托管提供程序

来源:互联网 发布:淘宝监控摄像头排行榜 编辑:程序博客网 时间:2024/04/29 17:41

剖析 .NET 托管提供程序

发布日期: 4/1/2004 | 更新日期: 4/1/2004

Dino Esposito

Wintellect

2001 年 10 月 9 日

与成熟的 OLE DB 提供程序相比,Microsoft .NET 托管提供程序有许多优点。首先,它实现了简化的数据访问结构,这种结构常常可提高性能,同时又不影响功能方面的能力。此外,.NET 托管提供程序通过方法和属性直接向使用者提供特定于提供程序的行为。它使用的接口集合也比 OLE DB 提供程序要少的多。最后但并非最不重要的是,.NET 托管提供程序工作在公共语言运行库 (CLR) 的边界内,无需 COM 交互。对于 SQL Server 7.0 和 SQL Server 2000 而言,托管提供程序直接挂接到线路级,获得了显著的性能优势。

*

.NET 数据提供程序提供的功能可分为以下两类:

通过 IDataAdapter 接口的方法的实现,支持 DataSet

支持连接的数据访问,包括表示连接、命令和参数的类

数据提供程序最简单的功能是,在读取和写入时只通过数据集与调用方交互。另一种情况是,您可以控制连接、事务处理以及执行直接命令,而不必考虑 SQL 语言。下图显示了 .NET 中两个标准托管提供程序(OLE DB 提供程序和用于 SQL Server 的提供程序)的类层次结构。


图 1. 托管提供程序连接、执行命令,并以特定于数据源的方式获取数据。

包装连接、命令和读取器的对象是特定于提供程序的,并且可能产生一组稍有不同的属性和方法。任何内部实现都严格地具备了数据库识别能力。在此架构范围之外的唯一的类是数据集。该类是所有提供程序所共有的,它作为断开连接的数据的一般容器。数据集类属于一种名为 System.Data 的超命名空间。特定于一个数据提供程序的类属于特定命名空间。例如,System.Data.SqlClientSystem.Data.OleDb 属于一个特定命名空间。上图显示的架构虽然并不过分简单,却是一种相当基本的架构。这是一种简化的架构,因为它不包括所涉及的所有类和接口。下图更接近真实情况。


图 2. 托管提供程序涉及的类

下表显示了构成 .NET 提供程序的接口的列表。

IDbConnection

表示一个与数据源建立的唯一会话

IDbTransaction

表示一个本地非分布式事务处理

IDbCommand

表示一个在连接到数据源时执行的命令

IDataParameter

允许将参数实现为命令

IDataReader

读取在执行命令后创建的只向前只读数据流

IDataAdapter

填充数据集并将数据集中的更改解析回数据源

IDbDataAdapter

提供对关系数据库执行典型操作(插入、更新、选择、删除)的方法

在所有接口中,只有 IDataAdapter 是强制性的,必须存在于每个托管提供程序中。如果您不打算实现其中的一个接口或给定接口的方法之一,无论如何都要公开该接口,但这样会引发 NotSupportedException 异常。只要有可能,应避免提供方法和接口的无操作实现,因为这种做法可能导致数据损坏,对于事务处理的提交/回滚尤其是这样。例如,提供程序无需支持嵌套事务处理,即使 IDbTransaction 接口的设计考虑到这种情况。

在进一步解释每个类在 .NET 提供程序的整体运行中所扮演的角色之前,我们先大概了解一下建议托管提供程序使用的命名约定。如果您打算自己编写提供程序,这方面的知识就非常有用。第一条准则是考虑命名空间。确保给自己的托管提供程序分配一个唯一的命名空间。接下来,使用在任何内部代码和客户端代码中标识提供程序的别名作为类的前缀。例如,使用 OdbcConnection、OdbcCommand、OdbcDataReader 等诸如此类的类名。在本文提到的情况中,别名为 Odbc。此外,尽量使用不同的文件来编译不同的功能。

实现连接

提供程序连接类从 IDbConnection 继承,并且必须公开 ConnectionString“状态”“数据库”ConnectionTimeout 属性。强制方法包括 “打开”“关闭”BeginTransactionChangeDatabaseCreateCommand。您不一定必须实现事务处理。下面的代码片断是一个用于实现连接的代码示例。

namespace DotNetMyDataProvider {  public class MyConnection : IDbConnection  {    private ConnectionState m_state;    private String m_sConnString;    public MyConnection () {m_state = ConnectionState.Closed;m_sConnString = "";    }    public MyConnection (String connString) {m_state = ConnectionState.Closed;m_sConnString = connString;    }    public IDbTransaction BeginTransaction() {      throw new NotSupportedException();    }    public IDbTransaction BeginTransaction(IsolationLevel level) {      throw new NotSupportedException();    } }}

您应该至少提供两个构造函数,其中一个是不带任何参数的默认构造函数。另一个建议的构造函数只接受连接字符串。通过 ConnectionString 属性返回连接字符串时,请确保返回的字符串始终是用户设置的那个字符串。唯一的异常可能是您也许希望删除的任何安全敏感信息导致的。

您在连接字符串中识别和支持的项取决于您本身,但无论何时,只要它有意义,就应该使用标准名称。“打开” 方法负责打开与数据源进行通信的物理信道。此操作不应该在调用 “打开” 方法之前进行。如果打开连接成为一个耗费内存的操作,可以考虑使用某种连接池。最后,如果期望提供程序在分布式事务处理中提供自动登记,登记应该在执行 “打开” 的期间进行。

使 ADO.NET 连接区别于其他连接(例如,ADO 连接)的一个要点是,您需要确保在可以执行任何命令之前创建和打开连接。客户端必须显式打开和关闭连接,没有任何方法会为客户端隐式打开和关闭连接。这种做法导致了某种程度的安全检查集中化。采用这种方法时,只有在获得连接后才执行检查,但提供程序中正巧涉及连接对象的所有其他类会同时受益。

方法 “关闭” 用于关闭连接。通常,“关闭” 应该只断开连接,并将对象返回池(如果有池)。您还可以实现 “处置” 方法来自定义对象的析构。连接的状态通过 ConnectionState 枚举数据类型来标识。当客户端使用连接时,您应该确保连接的内部状态与 “状态” 属性的内容相匹配。例如,当您提取数据时,将连接的 “状态” 属性设置为 ConnectionState.Fetching。

返回页首返回页首

ODBC 连接

现在我们来看一个具体的 .NET 托管提供程序如何在实践中应用上述原则。为此,我们以一个最新托管提供程序为例,尽管它只出现在早期的测试版本中。这就是 ODBC 数据源的 .NET 提供程序。您可能已经注意到,OLE DB 的 .NET 提供程序在连接字符串中不支持 DSN 标记。这样的名称需要自动选择 MSDASQL 提供程序并搜索 ODBC 源。下面的代码显示了 ODBC.NET 如何声明其连接类:

public sealed class OdbcConnection : Component, ICloneable, IdbConnection

OdbcConnection 对象利用了 ODBC 的典型资源,例如,环境句柄和连接句柄。这些对象使用类私有成员在内部存储。该类同时用于 “关闭”“处置”。通常,您可以使用其中的任何一种方法来关闭连接,但必须在连接对象超出作用范围之前使用。否则,内存的释放(也就是 ODBC 句柄)将留给垃圾回收器,而您是无法控制其执行时间的。在连接池方面,OdbcConnection 类依靠 ODBC 驱动程序管理器的服务。

为了使用 ODBC.NET 提供程序(目前提供 Beta 1),您应该将 System.Data.Odbc 包括在内。这样可以确保提供程序使用用于 JET、SQL Server 和 Oracle 的驱动程序。

返回页首返回页首

实现命令

命令对象为某些操作创建请求并将请求传递到数据源。如果返回结果,命令对象负责将结果作为定制的 DataReader 对象、标量值和/或通过输出参数进行打包和返回。根据数据提供程序的特性,您可以安排结果采用其他格式。例如,如果命令文本包括 FOR XML 子句,用于 SQL Server 的托管提供程序允许以 XML 格式获得结果。

类必须至少支持 CommandText 属性,并且至少支持文本命令类型。命令的分析和执行取决于提供程序。这是使提供程序有可能接受作为命令的任何文本或信息的一个关键要素。支持命令行为不是强制的,如果需要,您可以支持更多完全自定义的行为。

在命令中,连接可以与一个事务处理相关联。如果重置连接 — 并且用户应该能够在任何时候更改连接 — 那么首先禁用相应的事务处理对象。如果支持事务处理,则设置命令对象的 “事务” 属性时,应考虑额外的步骤,以确保您使用的事务处理已经与命令使用的连接相关联。

命令对象使用两个表示参数的类。一个类是 xxxParameterCollection,通过 Parameters 属性访问;另一个类是 xxxParameter,它表示集合中存储的单个命令参数。当然,xxx 代表特定于提供程序的别名。对于 ODBC.NET,这两个类就是 OdbcParameterCollection 和 OdbcParameter。

您可以对参数类使用 “新的” 运算符或通过命令对象的 CreateParameter 方法来创建特定于提供程序的命令参数。新创建的参数通过 Parameters 集合的方法填充和添加到命令的集合中。然后,用于命令执行的模块负责通过参数收集数据集。使用命名的参数(就像 SQL Server 提供程序那样)还是使用 ? 占位符(类似于 OLE DB 提供程序)则取决于您。

必须有一个有效的并且已打开的连接才能执行命令。使用任何标准类型的命令(例如,ExecuteNonQuery、ExecuteReader 和 ExecuteScalar)来执行命令。另外,还要考虑为 “取消”Prepare 方法提供实现。

返回页首返回页首

ODBC 命令

OdbcCommand 类不支持通过 SQL 命令和存储过程传递命名的参数。您必须改用 ? 占位符。至少在这个早期版本中,它既不支持 “取消”,也不支持 Prepare。正如您预期的那样,ODBC .NET 提供程序要求 Parameters 集合中命令参数的数目与命令文本中找到的占位符的数目要匹配。否则,就会引发异常。下面的代码行显示了如何将一个新的参数添加到 ODBC 命令,同时为该参数赋值。

cmd.Parameters.Add("@CustID", OdbcType.Integer).Value = 99

注意,提供程序定义了自己的一组类型。枚举 OdbcType 包括 ODBC 的低级 API 确实可以识别的所有类型(并且只包括这些类型)。原来的 ODBC 类型非常相似,例如,SQL_BINARY、SQL_BIGINT 或 SQL_CHAR 和 .NET 类型。尤其是,ODBC 类型 SQL_CHAR 映射到 .NET String 类型。

返回页首返回页首

实现数据读取器

数据读取器是提供程序为了使客户端以只向前方式读取数据而创建的一种已连接无缓存缓冲区。读取器的实际实现取决于提供程序的编写器。不过,应该注意遵循几条准则。

首先,DataReader 对象被返回用户时,应该始终处于打开状态并位于第一个记录之前。另外,用户不能直接创建 DataReader 对象。必须由命令对象创建和返回读取器。为此,您应该将构造函数标记为内部。采用 C# 时应使用关键字 internal

internal MyDataReader(object resultset){...}

采用 Visual Basic® .NET 时使用关键字 friend

Friend Sub New(ByRef resultset As object)      MyBase.New      ...End Sub

数据读取器必须至少有两个构造函数,一个接受查询的结果集,另一个接受用于执行命令的连接对象。只有当命令必须以 CommandBehavior.CloseConnection 的形式执行时才需要连接。在这种情况下,当 DataReader 对象关闭时,连接必须自动关闭。在内部,结果集可以采用满足您的需要的任何形式。例如,可以将结果集实现为数组或字典。

数据读取器应该正确管理属性 RecordsAffected。该属性只应用于包括插入、更新或删除命令的批处理语句。它通常不应用于查询命令。当读取器关闭时,您可能希望禁止某些操作和更改读取器的内部状态,以清理内部资源,如用于存储数据的数组。

数据读取器的 Read 方法始终前进到一个新的有效行(如果有任何新的有效行)。更重要的是,它应该只是使内部数据指针指向前,但不进行任何读取。实际的读取由其他特定于读取器的方法来完成,例如,GetStringGetValues。最后,NextResult 移到下一个结果集。基本上,它是将一个新的内部结构复制到 GetValues 等方法从其中进行读取的公用知识库。

返回页首返回页首

ODBC 数据读取器

像所有读取器类一样,OdbcDataReader 是密封的,并且是不可继承的。访问列值的类的方法自动强制它们返回的数据的类型采用最初从该列检索到的数据的类型。从某个给定列第一次读取一个单元格时使用的类型将用于同一列的所有其他单元格。也就是说,您不能从同一列中接连将数据作为字符串和长整型读取。

当命令对象的 CommandType 属性设置为 StoredProcedure 时,CommandText 属性必须使用过程的标准 ODBC 转义序列进行设置。与其他提供程序不同的是,对于 ODBC.NET 提供程序,仅仅使用过程的简单名称是不够的。下面的模式说明了通过 ODBC 驱动程序调用存储过程的典型方法。

{ call storedproc_name(?, ..., ?) }

字符串必须用 {...} 括起来,并且将关键字调用放在实际名称和参数列表之前。

返回页首返回页首

实现数据适配器

成熟的 .NET 数据提供程序提供继承了 IDbDataAdapterDbDataAdapter 的数据适配器类。类 DbDataAdapter 实现了用于关系数据库的数据适配器。不过,在其他情况下,您需要的是实现 IDataAdapter 接口并将某些断开连接的数据复制到内存中的可编程缓冲区(如数据集)的类。实际上,在大多数情况下,实现 IDataAdapter 接口的 Fill 方法对于通过数据集对象返回断开连接的数据已经足够了。

DataAdapter 对象的典型构造函数是:

XxxDataAdapter(SqlCommand selectCommand) XxxDataAdapter(String selectCommandText, String selectConnectionString) XxxDataAdapter(String selectCommandText, SqlConnection selectConnection)

从 DbDataAdapter 继承的类必须实现所有成员,如果使用特定于提供程序的功能,还必须定义额外的成员。最后,必须实现下面的方法:

Fill(DataSet ds)FillSchema(DataSet ds, SchemaType st)Update(DataSet ds)GetFillParameters()

需要的属性包括:

TableMappings(默认为空集合)

MissingSchemaAction(默认为 Add

MissingMappingAction(默认为 Passthrough

您可以根据需要提供 “填充” 方法的任意数目的实现。

表映射控制着源表(即数据库表)映射到父数据集中的数据表对象时采用的方式。映射考虑表名及列名和属性。而架构映射则考虑开始将新数据添加到现有数据集时处理列和表的方式。缺少的映射属性的默认值要求适配器创建像源表一样的内存中的表。缺少的架构属性的默认值处理实际填充数据表对象时可能产生的问题。如果目标数据集中缺少任何映射的元素(表和列),则 MissingSchemaAction 的值会建议采取什么措施。在某种意义上,这两个 MissingXXX 属性是一种异常处理程序。值 Add 强制适配器添加经证明缺少的任何表或列。除非为属性分配另一个 (AddWithKey) 值,否则,不添加任何关键信息。

当应用程序调用 “更新” 方法时,该类检查数据集中每一行的 RowState 属性,然后执行所要求的 INSERT、UPDATE 或 DELETE 语句。如果该类不提供 UpdateCommandInsertCommandDeleteCommand 属性,但实现 IDbDataAdapter,那么您可以尝试即时生成命令或产生一个异常。您还可以提供自定义的命令生成器类来帮助进行命令生成。

ODBC 提供程序提供 OdbcCommandBuilder 类作为自动生成单表命令的方法。OLE DB 提供程序和 SQL Server 提供程序提供了相似的类。如果您需要更新交叉引用的表,那么您可能要使用存储过程或即席 SQL 批处理。在这种情况下,只需覆盖 InsertCommandUpdateCommandDeleteCommand 属性,使它们运行您指定的命令对象。

返回页首返回页首

小结

.NET 数据提供程序提供的功能可分为两个主要类别:

支持断开连接的数据集对象

支持连接的数据访问,包括连接、事务处理、命令和参数

.NET 中的数据提供程序通过 IDataAdapter 接口的实现来支持数据集对象。还可以通过实现 IDataParameter 接口支持参数化的查询。如果您无法负担断开连接的数据,可通过 IDataReader 接口使用 .NET 数据读取器。

返回页首返回页首

对话栏:为多个结果集命名

Visual Studio® .NET 有一个很好的功能,即,您可以为数据适配器即将生成的所有表分配一个一致的名称。在任何 .NET 应用程序中配置了数据适配器对象之后,该对话框显示要创建的表的标准名称:Table、Table1、Table2 等等。对于其中的每个名称,您都可以在后来一次性指定为更形象的名称。我们是否可以通过某种方法以编程方式达到此目的呢?

Visual Studio .NET 是一个出色的产品,但它的使用需要一些技巧。上述问题的回答是 — 我们的确可以通过某种方法用编程方式来实现该目的,顺便提一句,Visual Studio 在后台也使用了同样的代码。

DataAdapter 对象有一个名为 TableMappings 的集合,其元素是 DataTableMapping 类型的对象。概括地说,表映射是什么?表映射是源表和适配器即将创建的相应数据表对象之间设置的动态关联。如果尚未设置任何映射,则适配器使用与源表相同的结构创建数据表对象,名称除外。名称是通过调用 “填充” 方法或字 “表” 指定的字符串。从多个结果集产生的额外的表在第一个表之后命名。因此,默认情况下,它们的名称分别是 Table1、Table2 等等。可是,如果数据适配器的填充如下面的代码所示,则额外的表分别命名为 Employees1、Employees2 等等。

myDataAdapter.Fill(myDataSet, "Employees");

在配置数据适配器时,Visual Studio 所做的是,为您以可视方式创建的每一个关联都创建一个 DataTableMapping 对象。下面的代码行显示了如何以编程方式为如上所述填充的数据集的前两个表分配有意义的具体名称。

myDataAdapter.TableMappings.Add("Employees", "FirstTable");myDataAdapter.TableMappings.Add("Employees1", "SecondTable");

第三个表(如果有)可以通过 Table2 访问。

虽然这是命名从多个结果集产生的数据表对象的最佳方法,但您也可以使用以下同样有效的代码:

myDataAdapter.Fill(myDataSet, "Employees");myDataSet.Tables["Employees1"].TableName = "SecondTable";

您还可以通过索引访问该表:

myDataSet.Tables[1].TableName = "SecondTable";

Dino Esposito 供职于 Wintellect,他承担了 ADO.NET 和 ASP.NET 方面的培训和咨询工作。他是 VB-2-The-Max 的创始人之一,并向 MSDN Magazine 的 Cutting Edge 专栏投稿。如果希望与 Dino 联系,可发送电子邮件至 dinoe@wintellect.com