Bob Beauchemin
DevelopMentor
适用于:
Microsoft ADO.NET 2.0
Microsoft Visual Studio 2005
C# 编程语言
摘要:了解在 ADO.NET 中对于从您的数据源访问元数据的增强支持。
下载相关的 SchemasSample.exe 示例代码。
本页内容
深入了解新的公共元数据 API
究竟谁需要元数据?
我能得到什么样的元数据?
Restrictions
DataSourceInformation
自定义并扩展元数据
用户自定义
小结:元数据支持的最终部分
深入了解新的公共元数据 API
在我的前一篇文章, 我指出 Visual Studio 2005 服务器资源管理器现在使用一个包含了 .NET 数据提供程序(而不是 OLE DB 提供程序)列表的对话框来提示连接信息。当您确定一个连接字符串并添加数据连接时,每个数据连接也显示一个关于通过连接直接可见的数据库对象(如表、视图 和存储过程)的信息的树形结构。但这些信息来自哪里呢?难道是 Visual Studio 仅为某些数据提供程序硬编码来生成这个信息,而假如我编写我自己的数据提供程序或者从第三方购买一个的时候,就留给我一个空节点?不,在 Visual Studio 2005 中并不是这样。由于 ADO.NET 2.0 中的新架构 API,所有这些有用的信息都将提供给您。我不知道这是否是 Visual Studio 做到它的方法,但是这里就是使用新的 API 来获得一个数据库中的表的列表的代码。
// uses a ADO.NET 2.0 named connection string in config file // uses ADO.NET 2.0 ProviderFactory and base classes // see previous article public static void GetListOfTables(string connectstring_name) { ConnectionStringSettings s = ConfigurationSettings.ConnectionStrings[connectstring_name]; DbProviderFactory f = DbProviderFactories.GetFactory(s.ProviderName); using (DbConnection conn = f.CreateConnection()) { conn.ConnectionString = s.ConnectionString; conn.Open(); DataTable t = conn.GetSchema("Tables"); t.WriteXml("tables.xml"); } }
究竟谁需要元数据?
元 数据是每个数据访问 API 的一部分。尽管元数据的主要使用者是像 Visual Studio 这样的工具或者像 DeKlarit 这样的代码生成器,但是它们并不是惟一的使用者。应用程序包设计师可能允许最终用户通过向现有的表添加新表或新列来自定义一个应用程序。当最终用户如此改 变了数据库架构时,一个通用查询和修改工具可以在维护、备份和其他应用程序函数中使用元数据来包含用户的新表,就像它们是应用程序附带的内置表一样。程序 员可以使用元数据来编写他们自己的派生自 System.Data.Common.DbCommandBuilder的自定义类,并为使用 DataSet 创建插入、更新和删除命令。多数据库应用程序(即设计为在用户选择的数据库上运行的应用程序)的构建者可以尽可能多地使用元数据来维护公共代码库,在需要时优化数据访问代码。
通过一般元数据 API 来公开元数据要比让每个使用者使用特定于数据库的 API 要好。这样,工具编写人员可以维护一个可管理性更好的代码库。这样的 API 还必须是非常灵活的,因为在编写一般 API 来公开元数据时有四个障碍需要考虑。
-
元数据集合和信息在数据库间是有差别的。例如,SQL Server 用户可能想要公开一个链接服务器的集合,但是 oracle 用户可能对关于 oracle 序列的信息感兴趣。
-
不仅在不同的数据库产品中,而且即使在相同数据库的不同版本中,存储公共数据库元数据的基础系统表都是不同的。例如,SQL Server 2005 使用在一个“sys”架构下的新表(例如,sys.tables)公开它的元数据,而 SQL Server 以前的版本使用元数据表(如 sysobjects£©来存储相同的数据。
-
不同的程序可能有不同的元数据视图。以一个例子来说,许多程序员抱怨 oracle 数据库中表的列表太长,因为大多数元数据 API 将“system”表与用户表混在一起。他们想要一个仅由他们定义的表组成的短列表。
-
是否根本不支持元数据,要提供多少元数据,应该完全取决于提供程序的编写者。
大 部分数据库 API 提供一个标准的元数据集,它是所有提供程序必须支持的,并且允许提供程序编写者添加新的元数据表。这与使用 ANSI SQL 标准采用的方法是一致的。解决这一问题的标准的一部分是 Schema Schemata(INFORMATION_SCHEMA 和 DEFINITION_SCHEMA)的第 11 部分。ANSI SQL INFORMATION_SCHEMA 定义了由一个兼容的数据库支持的标准的元数据视图集。但即便是这个规范也需要有一个方法来解决上面这些问题。它声明:
“实施者可以自由添加额外的表到 INFORMATION_SCHEMA 或添加额外的列到预定义的INFORMATION_SCHEMA 表。”
OLE DB 作为一个与 ANSI SQL 标准概念一致的数据访问 API 示例,定义了一系列的名为“架构行集合”的元数据。它从一个大致遵循 INFORMATION_SCHEMA 的预定义集开始,并添加 OLE DB 特定的列到每个行中。ADO.NET 2.0 提供了一个甚至更强大更灵活的机制来公开元数据。
我能得到什么样的元数据?
ADO.NET 2.0 允许提供程序编写者公开 5 种不同类型的元数据。这些主要的元数据 — “元数据集合”或“类别” — 在 System.Data.Common.DbMetaDataCollectionNames 类中被枚举出来。
-
MetaDataCollections — 可用元数据集合的列表。
-
Restrictions — 对于每个元数据集合,存在一批可以用于限制被请求的架构信息范围的限定符。
-
DataSourceInformation — 关于数据提供程序引用的数据库实例的信息。
-
DataTypes — 一组关于数据库支持的每个数据类型的信息。
-
ReservedWords — 适用于该种数据库查询语言的保留字。通常“查询语言”等同于一种 SQL 的方言。
MetaDataCollections 是 INFORMATION_SCHEMA 集合的名称,如“表”、“列”或“主键”。但是,如果使用 DbConnection.GetSchema,这些元数据类别也被认为是元数据。这意味着在代码方面来说是这样,这些集合可以像普通的元数据一样被获得。
// gets information about database Views Table t1 = conn.GetSchema("Views"); // gets information about collections exposed by this provider // this includes the five "meta-collections" described above Table t2 = conn.GetSchema(DbMetaDataCollectionNames.MetaDataCollections); // gets information about the Restrictions meta-collection Table t3 = conn.GetSchema(DbMetaDataCollectionNames.Restrictions); // No argument overload is same as asking for MetaDataCollections Table t4 = conn.GetSchema();
5 个元数据集合中的 2 个值得进一步解释。
Restrictions
Restrictions 可以用来限制返回元数据的数量。如果您熟悉 OLE DB 或 ADO,那么术语“restriction”意味着在那些 API 中同样的内容。作为示例,让我们使用 MetaDataCollection“列”,它是表中的列名称的集合。这个集合可以用于获取所有表中的所有列。但是,此被请求的列集合会被数据库名称、 所有者/架构或者表限制。每个元数据集合可以有不同数量的可能限制,并且每个限制会有一个默认值。在我们下面的示例中,这里是一个对列元数据的限制的 XML 表示:
清单 1. 列集合上的 Restrictions(XML 格式)
<Restrictions> <CollectionName>Columns</CollectionName> <RestrictionName>Catalog</RestrictionName> <RestrictionDefault>table_catalog</RestrictionDefault> <RestrictionNumber>1</RestrictionNumber> </Restrictions> <Restrictions> <CollectionName>Columns</CollectionName> <RestrictionName>Owner</RestrictionName> <RestrictionDefault>table_schema</RestrictionDefault> <RestrictionNumber>2</RestrictionNumber> </Restrictions> <Restrictions> <CollectionName>Columns</CollectionName> <RestrictionName>Table</RestrictionName> <RestrictionDefault>table_name</RestrictionDefault> <RestrictionNumber>3</RestrictionNumber> </Restrictions> <Restrictions> <CollectionName>Columns</CollectionName> <RestrictionName>Column</RestrictionName> <RestrictionDefault>column_name</RestrictionDefault> <RestrictionNumber>4</RestrictionNumber> </Restrictions>
Restrictions 是使用一个重载的 DbConnection.GetSchema 指定的。这些限制被指定为一个数组。您可以将一个数组指定为和整个限制集合或者一个子集数组一样大,因为“RestrictionNumbers”通常从 最少限制向最多限制发展。对您想要省略的限制值使用空值(不是数据库 NULL,而是 .NET NULL,或者在 Visual Basic .NET 中的 Nothing)。例如:
// restriction string array string[] res = new string[4]; // all columns, all tables owned by dbo res[1] = "dbo"; DataTable t1 = conn.GetSchema("Columns", res); // clear collection for (int i = 0; i < 4; i++) res[i] = null; // all columns, all tables named "authors", any owner/schema res[2] = "authors"; DataTable t2 = conn.GetSchema("Columns", res); // clear collection for (int i = 0; i < 4; i++) res[i] = null; // columns named au_lname // all tables named "authors", any owner/schema res[2] = "authors"; res[3] = "au_lname"; DataTable t3 = conn.GetSchema("Columns", res); // clear collection for (int i = 0; i < 4; i++) res[i] = null; // columns named au_lname // any tables, any owner/schema res[3] = "name"; DataTable t4 = conn.GetSchema("Columns", res);
您无需指定整个限制数组。在上面的情况中,这里您只想看到“dbo”拥有的表中的列,您可以指定一个只带有两个而不是全部四个成员的 数组。也请注意,将一个空字符串指定为一个限制与指定一个 null(在 Visual Basic .NET 中为 Nothing)值是不同的。您不必记住这些限制,您始终可以查询它们,就像任何其他集合一样。“Restrictions”集合本身不允许限制,但是因 为信息被提取到 DataTable 中,所以您可以使用一个 DataView 来提供相似的功能,如下所示。
DataTable tv = conn.GetSchema(DbMetaDataCollectionNames.Restrictions); DataView v = tv.DefaultView; // show restrictions on the "Columns" collection, sorted by number v.RowFilter = "CollectionName = 'Columns'"; v.Sort = "RestrictionNumber"; for (int i = 0; i < tv.Count; i++) Console.WriteLine("{0} (default){1}", tv.Rows[i]["RestrictionName"], tv.Rows[i]["RestrictionDefault"]);
DataSourceInformation
DataSourceInformation 集合为查询生成器提供关于当前数据库(数据源)实例的信息。虽然这个集合可包含提供程序需要的任何内容,但在 Microsoft 提供程序 (SqlClient、OracleClient、OleDb、Odbc) 中,这个集合包含相似的信息。这里是您默认获得的信息。
表 1. 在 Microsoft 提供程序中的 DataSourceInformation
值 |
格式/意义 |
---|---|
CompositeIdentifierSeparatorPattern |
多部分名称的分隔符(如 pubs.dbo.authors 中的点) |
DataSourceProductName |
数据库名称 |
DataSourceProductVersion |
数据库版本。请注意这是当前通过 DbConnection 访问的数据库实例的版本。 |
DataSourceProductVersionNormalized |
? |
GroupByBehavior |
枚举,System.Data.Common.GroupByBehavior |
IdentifierPattern |
正则表达式字符串 |
IdentifierCase |
枚举,System.Data.Common.IdentifierCase |
OrderByColumnsInSelect |
布尔值,默认情况下您应该在一个 Select 语句中 orDER BY 这些列 |
ParameterMarkerFormat |
说明参数标记是否以一个特殊的字符开始(如 T-SQL 中的 @) |
ParameterMarkerPattern |
用于创建参数的正则表达式字符串 |
ParameterNameMaxLength |
参数的最大长度 |
ParameterNamePattern |
用于创建参数的正则表达式字符串 |
QuotedIdentifierPattern |
用于引用标识符的正则表达式字符串 |
QuotedIdentifierCase |
枚举,System.Data.Common.IdentifierCase |
StatementSeparatorPattern |
正则表达式字符串 |
StringLiteralPattern |
正则表达式字符串 |
SupportedJoinOperators |
枚举,System.Data.Common.SupportedJoinOperators |
对特定的数据库方言来说有太多的信息来产生 SQL 了,您不这样认为么?还有一条信息我想要说一下,那就是提供程序是否在参数化查询中使用命名参数或者位置参数。在我关于编写独立于提供程序的代码的上一篇文章中,我将命名和位置参数作为编写参数化命令的两种方法来讨论。
自定义并扩展元数据
既 然我们已经看到了提供的基础元数据并且可以围绕 DbConnection.GetSchema() 找到我们的方法,让我们讨论提供程序编写者使用简单的声明性格式自定义元数据的方法,以及程序员如何能够挂钩到那种格式中。这个讨论与文章开始的元数据复 杂性相关:如何提供独立于数据库版本的元数据以及如何处理不同使用者可能需要相同元数据的不同视图的事实。
首先,让我们指出元数据支持完全是可选的。提供程序不必支持 DbConnection.GetSchema, 这种方法会引发 NotSupportedException。此外,如果提供程序编写者选择支持 DbConnection.GetSchema,只有 MetaDataCollections 类别是必需的。提供程序可以选择不提供任何或所有其他 4 个类别的信息。
其次,每个提供程序可以公开相同元数据集合的不同信息。例如,Tables 集合的结构完全取决于提供程序编写者。举个例子,SqlClient 提供程序在 Tables 集合中公开了 4 个信息项:table_catalog、table_schema、table_name 和 table_type。OracleClient 提供程序仅公开了 3 个信息项(OWNER、TABLE_NAME 和 TYPE),因为 oracle 数据库不包含多种目录。对每个提供程序来说限制和限制项的数量可以是不同的。再次以表的情况为例,SqlClient 提供程序支持 4 个限制,而 oracleClient 提供程序只支持 2 个。限制也不必按照任何指定的顺序发生。所以与在 OLE DB 和 ODBC API 中不同,这里没有委托管理的元数据结构、大量元数据或者元数据顺序。提供程序可以自由公开任何相关的元数据。但是,如果一个特定的应用程序(如 Visual Studio)要求在一个应用程序里使用的所有 .NET 数据提供程序中元数据是一致的,它也能够通过覆盖提供程序的标准行为来获得这个行为。我们将在后面的用户自定义小节进一步讨论这个问题。
提供程序编写者可以将元数据逻辑直接硬编码到他们的提供程序中,每个提供程序编写者为了获得相似的元数据可以使用不同的内部算法。例如,在实现 OLE DB 的 ISchemaRowset 方法时,这就是过去完成它的方法。但是,在 ADO.NET 2.0 中,在 System.Data.ProviderBase 命名空间中有一些基类是提供程序编写者可用的。4 个可用的 Microsoft 提供程序使用这些基类,因此它们都以相似的方法实现架构。我将用这个实现进行讲解,希望大多数编写提供程序的程序员喜欢 DataDirect 技术,而其他提供程序编写者也使用它。
公开元数据的基类是 DbMetaDataFactory。 实现它的一个子类的提供程序使用一个 XML 文件来定义其元数据提取行为。这些文件是在 System.Data.dll 和 System.Data.OracleClient.dll 中嵌入的资源,您可以通过从命令行运行 ILDASM.exe 来查看原始的 XML 文件。
>ildasm.exe System.Data.dll /out:dummy.il
查看从 ILDASM 生成的 XML 资源文件,如同剥开了洋葱的另一层。这个文件枚举了被支持的集合以及包含在每个元数据集合(通过架构)中的信息,并且看起来是使用 DataSet.WriteXml(XmlWriteMode.WriteSchema) 重载的 DataSet.WriteXml 方法的输出。最有趣的部分是在除了 DataSourceInformation 和 MetaDataCollections 元素中的 PopulationMechanism/PopulationString 子元素以外的所有元数据集合中的 MinimumVersion/MaximumVersion 元素。
使 用 MinimumVersion/MaximumVersion 允许提供程序编写者指定对于不同版本的数据库要执行哪些元数据查询。通过使用单个 MetaDataCollection 的多个元素,您可以使 GetSchema 对不同版本的数据库表现不同。举一个明显的例子,您可以使用与以前版本的 SQL Server 不同的 SQL Server 2005 各版本。这里是一个使用来自 SQL Server 元数据资源 (System.Data.SqlClient.SqlMetaData) 的 MinimumVersion 的例子:
清单 2. 数据类型集合中的数据类型 XML 的元数据项
<DataTypes> <TypeName>xml</TypeName> <ProviderDbType>25</ProviderDbType> <ColumnSize>2147483647</ColumnSize> <DataType>System.String</DataType> <IsAutoIncrementable>false</IsAutoIncrementable> <IsCaseSensitive>false</IsCaseSensitive> <IsFixedLength>false</IsFixedLength> <IsFixedPrecisionScale>false</IsFixedPrecisionScale> <IsLong>true</IsLong> <IsNullable>true</IsNullable> <IsSearchable>true</IsSearchable> <IsSearchableWithLike>false</IsSearchableWithLike> <MinimumVersion>09.00.000.0</MinimumVersion> <IsLiteralSupported>false</IsLiteralSupported> </DataTypes>
这定义了关于 SQL Server 数据类型 XML 的信息。MinimumVersion 指示出,这个数据类型只在使用 SQL Server 2005 时可用。如果您要求 SqlConnection.GetSchema 提供数据库支持的数据类型列表,只有 SQL Server 2005 的各版本数据库(SQL Server 2005 是版本 9,当前的beta 2 版是 09.00.852.2)会报告它们支持 XML 数据类型。
对于通常被 INFORMATION_SCHEMA(如表、视图或存储过程)公开的集合,PopulationMechanism 和 PopulationString 是工作开始的地方。在这个实现中使用了三个 PopulationMechanisms:DataTable、SQLCommand 和 PrepareCollection。DataTable 用于填充元数据集合。使用 DataTable 意味着用于填充集合的信息是在 XML 资源文件本身当中。在每种情况下,PopulationString 是当 XML 资源文件加载到一个 .NET DataSet 时,生成的 DataTable 的名称。SQLCommand 意味着提供程序将使用一个 DbCommand 实例来发出对数据库的命令。如果您查看由 SQLCommand 产生的集合的一个 PopulationString:
清单 3. SQL Server 中的数据库(目录)项 — MetaDataCollection
<MetaDataCollections> <CollectionName>Databases</CollectionName> <NumberOfRestrictions>1</NumberOfRestrictions> <NumberOfIdentifierParts>1</NumberOfIdentifierParts> <PopulationMechanism>SQLCommand</PopulationMechanism> <PopulationString>select name as database_name, dbid, crdate as create_date from master..sysdatabases where name = {0}</PopulationString> </MetaDataCollections>
当限制在 DbConnection.GetSchema 中使用时,很容易就能够推断出字符串替换将被应用到“基查询”中。如果没有限制被指定,那么实际上该谓词将被剥离出查询。
当 PopulationMechanism 的值为 PrepareCommand 时,提供程序编写者可以使用自定义机制。有一个 DbMetaDataFactory 的 PrepareCommand 方法,如果它被提供程序编写者覆盖,就可以被编码来使用提供程序选择的任何自定义语义。这一机制被用在 SqlClient 中来生成 DataTypes 元数据集合。PrepareCommand 的 SqlMetaDataFactory 子类实现首先从 DataTable 中读取由 SQL Server 支持的内置数据类型,如同用其他元数据集合一样;然后,如果数据库是 SQL Server 2005 的话,使用自定义逻辑来将用户定义的类型添加到集合中。(注:SQL Server 2005 可以将 .NET 类作为用户定义的类型公开。更多信息请参阅 A First Look at SQL Server 2005 for Developers 的第 5 章。)
用户自定义
除了提供程序自定义机制外,也有允许程序员在每个应用程序基础上自定义架构信息的挂钩!在加载嵌入资源之前,DbConnectionFactory CreateMetaDataFactory 将参考应用程序配置文件。每个提供程序都可以用任何它选择的方法实现 CreateMetaDataFactory 来为其 DbMetaDataFactory 获取 XML 流,但是那 4 个 Microsoft 提供程序遵循一个公共模式。每个 Microsoft 提供程序将查找一个按提供程序本身命名的应用程序配置设置(如 system.data.sqlclient)。在这个设置元素中您可以 添加或删除名值对。DbMetaDataFactory 查找一个名称“MetaDataXml”。与特殊名称对应的值是一个文件的名称。这是一个简单的文件名 — 这个文件必须存在于 .NET 安装位置的 CONFIG 子目录中。这是 machine.config 和安全配置设置所在的目录。这个文件必须包含整个的架构配置信息集,而不仅仅是那些更改。
出于多个原因,对于支持此机制的提供程序, 您可以使用这个机制。例如,您可以更改 oracleClient 提供程序中的架构查询来使用“USER”目录视图而不是“ALL”目录视图。因为“USER”视图不包含关于内部数据库表的信息,例如,表列表将会短很多 并且更易于使用。另一个示例可能包括为所有的 .NET 数据提供程序编码输出元数据 XML 文件,这提供给您一致的标准元数据集,该元数据集可能正好符合 SQL-99 INFORMATION_SCHEMA 视图。这可能恰恰适合您的应用程序。
一个更具体的例子是,如果我想要公开 SQL Server 2005 中关于 SQL Server Service Broker 的元数据集合的信息。这些集合可以包括 QUEUE、SERVICE、CONTRACT 和消息类型。我会从嵌入的 XML 资源文件开始并用我的新集合上的信息修饰它。如果这个文件的名称是 SQLBrokerAware.xml,我会安装这个文件,我的应用程序配置文件将为如下所示:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.data.sqlclient> <settings> <add name="MetaDataXml" value="SQLBrokerAware.xml"> </add> </settings> </system.data.sqlclient> </configuration>
这就是所有的步骤。使用这个设置,我可以编写代码 — 在其中 Service Broker 元数据是可用于客户端的内置元数据的组成部分。对所有队列来说,代码可能如下所示:
using (SqlConnection conn = new SqlConnection(connstring)) { conn.Open(); // this includes Service Broker metadata collections Table t = conn.GetSchema(DbMetaDataCollectionNames.MetaDataCollections); // get all the queues in my database Table queues = conn.GetSchema("Queues"); }
真是非常酷!本文包含一个添加 Service Broker 元数据的代码示例。 尽管这是一个非常强大的功能,它还是有可能被滥用的。请记住您需要将元数据 XML 文件分发到每个使用它的应用程序,并说服系统管理员为您将其安装在 CONFIG 目录中。而且您需要用发布的提供程序的每个新版本来维护它。因为使用一般元数据 API 的理由之一是可拥有跨数据库和应用程序的一致元数据,所以这个功能不应被无故使用。还需注意此时您不能提供 PrepareCommand 的自定义实现。
关于自定义的最后要注意的,您可能已经猜到了,自定义和资源对于 OLE DB 和 ODBC 的桥提供程序的使用是不同的。当您使用这些提供程序时,默认的 Odbc 或 OleDb XML 资源文件被使用,然后您不仅可以自定义主要的 Odbc 或 OleDb 架构行为,而且可以在每个提供程序基础上自定义行为。如果您想要指定自己的提供程序或者驱动程序,在添加/删除设置子元素中使用的名称属性不能是 MetaDataXml,而应该是 [providershortname]:MetaDataXml。如果您想让您的文件成为 OleDb 或 Odbc 数据提供程序的默认文件,您甚至可以指定 defaultMetaDataXml 的名称。
小结:元数据支持的最终部分
在结束前,我只想说说其他两个不是通过 DbConnection.GetSchema 公开的元数据扩展。 DbCommandBuilder 包含两个属性,QuoteIdentifier 和 UnquoteIdentifier,它们允许您在由 CommandBuilder 生成的命令中自定义标识符。举个例子,根据您的会话设置,在 SQL Server 中您可以使用双引号 (") 或括号('[' 和 ']')来引用标识符。最后,在 System.Data.Sql 命名空间中的 SqlMetaData 类被用于公开 SQL Server DataReader 中的元数据并允许您在 SqlCommand 使用的 Parameters 中 设置这个元数据。虽然不是任何地方在细节上都非常接近,但是这个元数据在概念上与在 DataTypes 元数据集合中公开的元数据相似。SqlMetaData 对 SqlClient 数据提供程序和 SQL Server 2005 包括的 in-database SQLServer 数据提供程序都是适用的。SqlMetaData 增大了当前通过 SqlDataReader.GetSchemaTable() 方法公开的行集合元数据。
此 时您很可能同意 ADO.NET 2.0 元数据基础结构是您所见过的最强大、灵活和可自定义的结构。它将 ADO.NET 发展成为一个完全面向对象的数据库 API。工具供应商、提供程序编写者和 Visual Studio 数据工具用户将会高兴地在街上跳舞 — 我们那里见。
Bob Beauchemin,DevelopMentor 的讲师、课程作者和数据库课程联络员。作为以数据为中心的分布式系统的架构师、程序员和管理员,他有超过 25 年的经验。他一直为 Microsoft Systems Journal 和 SQL Server Magazine 等杂志撰写 ADO.NET、OLE DB 和 SQL Server 方面的文章,并且是 A First Look at SQL Server 2005 for Developers 和 Essential ADO.NET 的作者。