爱心技术专栏专题

托管UDT使您能够扩展SQLServer的类型系统

摘录:数据库区 - MS SQL 来源:数据库区 - MS SQL 加入时间:2007年04月30日
摘要:
托管UDT使您能够扩展SQLServer的类型系统
托管 UDT 使您能够扩展 SQL Server 的类型系统

本文基于 Microsoft SQL Server 代号“Yukon”的 Beta 1 版本,文中所有信息都有可能发生变化。

下载本文的代码: (112KB)

注:本文是在产品投放生产之前编写的,因此,我们无法保证此处包含的任何细节都与在交付使用的产品中发现的细节完全一致。文中信息描述的是本文发布之时的产品,仅供规划之用。这些信息可在任何时候更改,恕不预先通知。

不透明或抽象类型 不透明或抽象类型
创建<a href=程序集" hspace="4" src="http://www.microsoft.com/library/gallery/templates/MNP2.Common/images/arrow_px_down.gif" width="7" vspace="2" border="0"/> 创建集
类属性 类属性
比较 UDT 的值 比较 UDT 的值
为空性 为空性
支持的转换 支持的转换
其他要求 其他要求
在 SQL Server 中使用 UDT 创建<a href=程序集" hspace="4" src="http://www.microsoft.com/library/gallery/templates/MNP2.Common/images/arrow_px_down.gif" width="7" vspace="2" border="0"/> 在 SQL Server 中使用 UDT 创建集
创建类型 创建类型
使用 UDT 使用 UDT
删除和更改 UDT 以及<a href=程序集" hspace="4" src="http://www.microsoft.com/library/gallery/templates/MNP2.Common/images/arrow_px_down.gif" width="7" vspace="2" border="0"/> 删除和更改 UDT 以及集
小结 小结

用户定义类型 (UDT) 是 SQL Server™ 下一个版本(代号“Yukon”)中新的公共语言运行库 (CLR) 的集成功能之一。Yukon 中的 UDT 标志着自 SQL Server 以前版本以来一个显著的进步。例如,SQL Server 2000 支持别名类型,可以为用户提供一种重新定义本机类型的简单方法。别名类型是使用现有数据类型创建的,现有数据类型在多个位置(表、过程等)以相同的方式定义。例如,一个邮政编码可能在多个表中使用,并作为多个存储过程中的一个参数。可以用如下方法创建一个名为 ZIP 的类型:

EXEC sp_addtype ZIP, CHAR(5), NOT NULL

接下来便可以在任何需要邮政编码的地方使用 ZIP,而不是 char(5) NOT NULL,并且也不必担心与同样存储邮政编码的其他表中的其他字段保持一致的问题。注意 ZIP 上有一个附加的约束 — NOT NULL。这一约束强制字段必须有一个值,因此使用该类型将不允许空值。您可以创建规则和默认对象并将它们绑定到别名类型,从而进一步地实施和维护数据完整性。例如,可以创建一个只允许数字值的规则并将其绑定到 ZIP 类型。这一技术有强大的设计和实现优势,例如更有组织的以及更一致的数据结构将产生更一致的数据。

在 Yukon 中,存储过程 sp_addtype 被新的数据描述语言 (DDL) 语法 CREATE TYPE 所替代。前面的例子可以写成下面这样:

CREATE TYPE ZIP FROM char(5) NOT NULL

Yukon UDT 允许用户编写 Microsoft .NET 框架类,这些框架类能够在 SQL 语言类型中注册为标量类型。这使得 Yukon UDT 与任何其他 SQL 本机类型地位相同。Yukon UDT 被编译成能够在内部注册并存储的 .NET 集。一旦集存储到中,用户能够使用 CREATE TYPE 语句的扩展将该集中的类定义为类型,相关内容将在本文的后面详细阐述。要让一个 .NET 框架类作为 SQL 类型中的类型,它必须实现一个协定 — 一组接口和方法,在 CREATE TYPE 时由 Yukon 进行验证。定义和实现 .NET 集中定义的 UDT 能够进行更为灵活的类型设计,不仅可以存储结构化的类型,而且还允许自定义方法、属性以及数据的序列化。

该功能有几个优点。它是扩展标量类型的一个强有力的方法。这一可扩展性机制非常健壮,您可以使用它在中存储类型的实例,以及在许多上下文中运行它们(作为变量、存储过程的参数以及函数的值),并在几乎所有能够使用本机类型的地方使用它们,包括诸如复制、大量复制、分布式查询和跨操作之类的高级场景中。另外,您可以将定义在类型上的行为作为查询的一部分来调用,甚至能对调用这种行为的结果建立索引以加速查询的执行。

UDT 的另一个优点是封装。类型的状态以及在类型上的操作常常可作为一个单元来使用。类型上的方法控制对状态的访问,使您能够自由地使用熟悉的 OO 范例来考虑类型,并产生其他应用开发人员能够使用的可重用组件。在本文的后面,我将讨论用 .NET 代码实现 UDT 的各种方面,以及在 Yukon 中使用结果类作为数据类型的问题。

不透明或抽象类型

Yukon UDT 可以作为不透明或者抽象数据类型,因为类型的使用者不了解类型执行的内部细节。他们仅通过类型的公共接口与类型进行交互。这一定义 UDT 的新技术为设计开启了新的大门。既然 UDT 可以是具有属性、方法等的托管类型,那么您现在可以创建类型来代表大量以前在 SQL Server 中没有的数据结构。使用 UDT,您能够创建代表地理空间(位置和地图类型的数据)、自定义二进制数据、编码数据以及加密数据的数据结构。

您可以使用实现 UDT 协定的托管代码来创建一个类,从而扩展 SQL 类型。接下来使用 CREATE ASSEMBLY 语句将包含 UDT 的已编译集加载到上的中,并使用公开托管代码 UDT 的 CREATE TYPE 语句在中创建类型。

这时,您可以在表定义或变量声明中使用该类型。让我们从检查 UDT 托管代码需求开始。

创建集

为了在 .NET 中创建可用于 Yukon 的集,有几点要求需要注意。这里,我先不讲述创建 .NET 集的基本内容。有关此问题的更多信息,请参阅 " target="_blank">Microsoft .NET tutorials 中的文章。

您将利用在 UDT 开发中用到的几个命名空间。为了使您的开发工作更为容易,加入如下指令:

using System.Data.Sql;using System.Data.SqlTypes;

System.Data.SqlTypes 命名空间包含能够在集中使用的代表 SQL Server 本机数据类型的类。System.Data.Sql 命名空间包含将被用作 UDT 的集里所必需的各种属性所需要的对象。所有这些类型存在于 System.Data.dll 中。

让我们看一下创建集(Yukon 使用)的 CLR 细节。在接下来的部分,我将分析不同的实现需求。如果您已具备 .NET 编程经验,那么对于其中一些内容您将会非常熟悉,而另外一些则是我刚刚提及的新命名空间所特有的内容。

类属性

对于 Beta 1 版,为了使类作为 UDT 使用,必须指定两个属性:Serializable 和 SqlUserDefinedType(如图 1 所示)。Serializable 属性使得类的数据能够被 CLR 序列化为可存储的格式,而 SqlUserDefinedType 则定义诸如序列化格式以及存储结构的最大容量之类的 UDT 特性。SqlUserDefinedType 属性有四个属性(参见server/art/UDTExtSQLSer02.htm" target="_blank">图 2),其中仅有一个是必需的。在将来的 beta 版中,将不再需要把 UDT 类标记为 Serializable。

sqlserver/art/UDTExtSQLSerfig01.gif" width="265" border="0"/>

图 1 类的属性

 

在 Format.SerializationFormat 中可用的三种序列化格式为:UserDefined、Native 以及 SerializedDataWithMetadata。顾名思义,UserDefined 序列化是用户定义的,并且必须用集代码来实现。例如,集可以进行某些自定义的二进制序列化。为此,该类必须实现 IBinarySerialize 接口,特别是它的 Write 和 Read 方法。这是最为灵活的序列化方法,并且能够像本机格式一样迅速(取决于 Write 和 Read 方法的实现)。

本机格式使用本机 SQL Server 二进制序列化。它是最快的,但同样也是最缺乏灵活性的。如果类的公共属性是固定长度的值类型数据类型,那么您可以只使用这种序列化格式。例如,如果属性为数值类型或者日期/时间类型,可以使用本机格式。但是,假如您要公开串值属性,将不能使用本机格式。

SerializedDataWithMetadata 使用 .NET 序列化。由于序列化可以自动处理,因此它非常灵活,并且能够为更多的数据类型所用,包括串以及引用类型。遗憾的是,它也是最慢的,其执行速度比本机格式慢了约一个数量级。更重要的是,在以后的 beta 版本中将不支持 SerializedDataWithMetadata。

同样,Microsoft 建议对于任何新代码不使用 SerializedDataWithMetadata。如果您已经使用,应该仅在开发过程中而决不要在产品中使用它。用 SerializedDataWithMetadata 创建的类应该转换为 UserDefined 格式并应实现 IBinarySerialize 接口,使用 Read 和 Write 方法来定义公共类成员的自定义序列化。

这里有一个有关这些类属性的示例,这些属性满足了 UDT 的最低要求:

// C#[Serializable][SqlUserDefinedType (Format.Native)] Visual Basic .NET<Serializable(), SqlUserDefinedType(Format.Native)>

正如您看到的,我使用了这两个必需的属性,而对于 SqlUserDefinedType 属性,我设置了它所需的属性。

比较 UDT 的值

在执行二进制比较时,IsByteOrdered 属性将影响到 SQL Server 如何使用 UDT 的实例。换句话说,如果 IsByteOrdered 为 false,那么 UDT 的二进制表示形式不可与另外的值相比较。这意味着作为该 UDT 定义的字段不能参与需要进行比较的操作,例如排序和索引。由于该字段不能在索引中使用,因而它不能是主键或者唯一键,这继而意味着它不具有参照完整性。该字段也不能用于 ORDER BY、GROUP BY 或者 PARTITION BY 子句。

当 IsByteOrdered 为 true 时,SQL Server 无需创建 UDT 的托管实例即可在所有的比较中使用该 UDT 的二进制表示形式。这将允许我前面提到过的功能及其他功能,包括可索引、主键和外键、检查和唯一约束、ORDER BY、GROUP BY 和 PARTITION BY 子句、顺序比较以及比较运算符。

如果您的 UDT 需要这些功能中的任何一种,则必需把 IsByteOrdered 设置为 true。但是,需要注意的一点是,只有当 UDT 的设计者能够保证序列化的二进制数据完全与信息的语义顺序相同时才能将 IsByteOrdered 设置为 true。为了更好地理解这一点,让我们来看一个例子。

如果 Point 类代表空间中的一点,X 和 Y(二者都定义为 Int32)是它仅有的属性,它能够作为二进制排序的候选吗? 换句话说,能够认为其中任何一个 Point 小于或大于另外一个 Point 吗? 例如,将 PointA (4, -1) 与 PointB (2, 3) 进行比较,其中哪个更大呢? 对于该问题没有一个简单的答案,正如没有一个简单的方法来存储二进制数据以反映这一顺序一样。因此,Point 不是一个能够实现二进制排序的类。

Yukon 只有在 UDT 值的序列化表示形式已经是二进制排序时,才支持在其值上使用比较运算符(以及依赖于比较支持的相关功能,如 GROUP BY 和 ORDER BY)。UDT 的设计者通过在类型定义中将 IsByteOrdered 设置为 true 来表明序列化表示形式具有此属性。同样,数据的二进制表示形式的排序必须与类型的语义排序等价。换句话说,在序列化字节上执行比较操作产生的结果与在托管代码中执行比较的结果相同。

应该注意的是,在本机格式中,如果设置了该属性,SQL Server 将保证二进制表示形式是可比较的。在 UserDefined 格式中,选取一个具有该属性的规范化算法是开发人员的责任。并且,三种序列化格式都支持等于 (=) 以及不等于 (!=) 比较运算符。Native 和 UserDefined 序列化格式另外还支持大于 (>)、小于 (<)、大于或等于 (>=) 以及小于或等于 (<=) 比较运算符。

为空性

尽管所有的 Yukon UDT 都识别空值,然而为了让 UDT 能够将空值接受为一个有效值,类必须实现 INullable 接口。这将由以下语句完成:

// C#public class MyClass: INullable Visual Basic .NETPublic Class MyClassImplements INullable

INullable 包含一个唯一的属性 IsNull,因而类必须这样实现:

// C#public bool IsNull{get {return is_Null;}} Visual Basic .NETPublic ReadOnly Property IsNull() As Boolean Implements INullable.IsNullGetReturn (is_Null)End GetEnd Property

is_Null 变量是私有变量,并保存实例的空状态。最后,类必须拥有一个名为 Null 的静态属性,以返回 UDT 的一个空值实例。如果实例确实为空,将允许 UDT 返回一个空值,如server/art/UDTExtSQLSer03.htm" target="_blank">图 3 中的代码所示。

支持的转换

UDT 必须支持到串的转换以及来自串的转换。为此,类需要在其接口中定义两个公共方法。第一个是静态方法 Parse,允许将串值转换为 UDT 实例。以下是该函数的定义:

// C#public static MyClass Parse(SqlString s) Visual Basic .NETPublic Shared Function Parse(ByVal s As SqlString) As MyClass

第二个方法是实例方法 ToString 的重写方法,如下所示:

// C#public override string ToString() Visual Basic .NETPublic Overrides Function ToString() As String

该方法使 UDT 转换为串值。虽然并非强制,不过这两种方法的正确实现应该是互逆的。也就是说,这两种方法应该能够使用 Parse 将串转换为 UDT,然后用 ToString 方法将其转换回原始的串值。

其他要求

除了我之前提到的功能之外,您应该知道还有几个其他的要求。由于类需要有一个构造函数,因此需要在 UDT 中实现它。UDT 类需要一个零参数公共构造函数,如下所示:

// C#public MyClass () { }

另外,可以创建其他的公共构造函数。这是类型的公共成员能够被重载的唯一时机。下面是在创建 UDT 时您应该知道的一些其他限制:

在 .NET 代码中不允许有可更改的静态数据成员。

不支持固定长度串以及固定长度二进制数据类型。

所有类名、方法名和属性名必须与 Yukon sysname 数据类型相一致,亦即您的公共名称不能超过 128 个。

不支持继承;Yukon 不能使用继承,尽管继承依然能够存在于 .NET 代码中。

注意,SQL 类型不会意识到 UDT 之间的继承层次结构。不过,UDT 的设计者能够用构造类的方式将继承当作一种实现机制。不能从 T-SQL 调用继承的方法,尽管您可以在类型的托管实现中调用这样的方法。

同样,如果您的确创建了一个重载方法,您应该预先警惕。当您在 SQL Server 中注册集或者创建类型时,它将不会被捕获。这是因为重载方法的检测发生在运行时而非类型创建之时。因此,只要不被调用,重载方法就能够存在于类中。如果该类被调用,将引发一个错误,错误提示为:“More than one method, property, or field was found with name method_name in class class_name in assembly assembly_name.”(在集 assembly_name 的类 class_name 中,找到不止一个方法、属性或字段的名称为 method_name。)不支持重载方法、属性或字段。同样,请注意这一规则有个例外,正如我在论及构造函数时所述。

当您需要将方法标记为确定性方法或者允许方法为更改方法时,需要用到 SqlMethod 属性。该属性有四个参数:

Deterministic 表示方法是确定性的 (true) 或者是非确定性的 (false) 。默认情况下为 false。

OnNullCall 如果 OnNullCall 为 false,当至少有一个输入参数为 NULL 时,不对方法求值就返回 NULL。如果 OnNullCall 为 true(这是默认值),不论输入什么参数,将通过对方法求值来确定结果值。

IsMutator 这一属性(默认时为 false)用以说明一个方法是否可以为实例的更改方法。默认时,方法调用不能更改实例的状态。如果创建了一个需要改变实例状态的属性,那么需要使用这一属性将其标记为更改方法。请注意,属性默认情况下为更改方法,并且不需要使用该属性。

DataAccess 该属性表明一个函数或方法是否包含有 SQL SELECT 语句。它能被设置为 DataAccessKind.None 或者 DataAccessKind.Read。

让我们回顾一下基于 CLR 的 UDT 实现的两个示例。第一个例子是一个简单的 Point 类,如server/art/UDTExtSQLSer04.htm" target="_blank">图 4 所示。该类型有两个属性 X 和 Y,以及一个公共方法 DistanceTo,用以返回当前 Point 到另一个作为参数传入的 Point 的距离。您将在本文的后面了解它在 Yukon 中的用法。注意,这也能很容易地定义为结构。

第二个例子是一个 Address 类(参见server/art/UDTExtSQLSer05.htm" target="_blank">图 5),存储地址、城市、州以及邮政编码数据。该类通过实现 IBinarySerialize 以及 Write 和 Read 方法演示了 UserDefined 的序列化格式。

在 SQL Server 中使用 UDT 创建集

一旦将该类编译为集,下一步便是在 SQL Server Yukon 中注册它。为此,首先需要使用 CREATE ASSEMBLY 语句装载集,这样便在当前中注册了集。请记住,如果想要在多个中使用一个集,必须分别在每个中注册集。其语法如下:

CREATE ASSEMBLY ASSEMBLY_NAME FROM PATH\ASSEMBLY_NAME.dll

注意 SQL 集名必须与 CLR 集名匹配,否则将不能注册。下面是一个示例:

CREATE Assembly YukonCLR FROMC:\Projects\"Yukon"\UDTs\Point\"Yukon"CLR.dll

被注册的集仅在当前的上下文中存在,并且存储在几个对象中。这里暗示,中的 DLL 仅在创建 DLL 的内部表示形式时被引用一次。完成集的注册后,其 DLL 能够从中删除,而 Yukon 中的 UDT 将继续正常工作。既然集被装载到当前的,它将不能被其他所使用。每一个需要集引用的必须自己调用 CREATE ASSEMBLY 语句。尽管这听起来显而易见,还是应该注意只有编译了的 DLL 被装载到中,而源代码并没有被装载。正如 SQL Server 中的所有对象一样,集名必须遵照 sysname 约定(最大长度为 128 )。

创建类型

看一下 CREATE TYPE 语句的语法:

CREATE TYPE [TYPE_SCHEMA_NAME.]TYPE_NAME EXTERNAL NAMEASSEMBLY_NAME:CLASS_NAME

只要装载了集,便可以使用 CREATE TYPE 语句向的可用类型列表中添加类型。与集一样,类型仅在当前中创建,而且与集一样也被 sysname 约定所限制。下面的示例将从我刚才展示过的 Visual Basic .NET CLR 代码中创建 Point UDT:

CREATE TYPE Point EXTERNAL NAME YukonCLR:Point

UDT 的名字(这里为 Point)对于上下文中给定的架构名必须唯一。这意味着如果有两个不同的集在中注册,且它们含有共同的类名,那么这些类中仅有一个能够被创建为同一个架构中里的类型。在下面的代码示例中,Point 类既存在于 YukonCLR 集中,同时也存在于 YukonCLR2 集中。一旦 YukonCLR 的 Point 类被创建为当前中的 UDT,YukonCLR2 中的 Point 类除非在一个不同的架构中创建,否则将不能被创建。现在,假设下面的示例在 MySchema 架构中创建:

CREATE Assembly YukonCLR FROM C:\Projects\Yukon\UDTs\Point\YukonCLR.dllCREATE Assembly YukonCLR2 FROM C:\Projects\Yukon\UDTs\Point\YukonCLR2.dllCREATE TYPE Point EXTERNAL NAME YukonCLR:Point                --OKCREATE TYPE Point EXTERNAL NAME YukonCLR2:Point--BAD - created in MySchemaCREATE TYPE DifferentSchema.Point EXTERNAL NAME YukonCLR2:Point--OK  - created in DifferentSchemaCREATE TYPE Point EXTERNAL NAME YukonCLR2:Triangle            --OK

使用 UDT

当类型注册为的可用类型之一后,就可以开始在创建对象(如表)时使用它,如下所示:

CREATE TABLE Points(PointID int NOT NULL,Pnt Point NOT NULL)

在表定义中使用 UDT 不需要特殊编码。使用与内部类型(如 int 或 nchar)一样的方式来定义表。

在我深入阐述之前,让我们看一下使用 Yukon 中用户定义类型的属性和方法的语法。它非常类似于 C# 和 Visual Basic .NET 的语法,只不过在这里属性或方法前面是两个冒号 (::) 而不是一个句点。到 Beta2 版时,:: 符号将要消失,而代之以句点符号。以下为属性和方法的语法:

Property Use Syntax: <implemented_type>::<property_name> =<scalar_expression>Method Use Syntax: <implemented_type>::<method_name>([arguments])

要使用 T-SQL 为表填充数据,可以运行如下脚本:

DECLARE @startPoint PointDECLARE @endPoint PointSET @startPoint = CAST(10:10 AS Point)SET @endPoint::X = 5SET @endPoint::Y = 3INSERT PointsVALUES(1, @startPoint)INSERT PointsVALUES(2, @endPoint)

对于任何使用过以前版本 SQL Server 的 T-SQL 变量、DML 语句以及 SELECT 语句的人来说,这些代码中许多都是很熟悉的。但是,还是有一些 T-SQL 代码的元素不太一样。第一个是使用 CAST 函数将 X 和 Y 的值赋给 @startPoint。当您以这种方式使用 CAST 函数时,将对 UDT 调用 Parse 方法以便为 UDT 实例填充数据。接下来,@endPoint 变量的 X 和 Y 属性被单独设置,从而使您能够显示地把值传递给 UDT 的属性。

代码的下一部分使用标准的 INSERT 语句把 UDT 类型的实例插入到 Points 表中。从存储在表中的 UDT 选择适当的值是通过语法 ColumnName::Property 或 ColumnName::Method 指定想调用的属性或方法的过程。即使方法不带参数,同样需要圆括号。在随后的示例中,您将从整个点集中选取 X 和 Y 的值:

SELECT Pnt::X AS XValue, Pnt::Y AS YValue FROM Points

在这里我将选取一点并将它存储到变量 @pt 中,然后使用 DistanceTo 方法检查该点到表中另外一点的距离:

DECLARE @pt PointSELECT @pt = Pnt FROM Points WHERE PointID = 2SELECT Pnt::DistanceTo(@pt) AS DistanceFROM PointsWHERE PointID = 1

下一个例子,如server/art/UDTExtSQLSer06.htm" target="_blank">图 6 所示,说明了如何在 Yukon 中使用 Address 类(假设其为 YukonCLR 集的一部分并且您已经创建了它的类型)。首先,创建类型 Address 的三个变量。其次,赋予地址信息并使用 cityStateZip 方法获取格式化的城市、州以及邮政编码。代码的第二部分利用 CAST 函数将第一个地址 (@addr) 复制到第二个地址 (@addr2) 中。这创建了一个副本,而不是对第一个变量的引用。随后对于 @addr 的任何改变将不会反映在 @addr2 中。最后,检查变量 @addr3 以确定它是否为空。由于该变量只是被声明而从未被赋值,它实际上为空,正如您看到的那样。

删除和更改 UDT 以及集

使用 DROP TYPE 语句可以很容易地删除 UDT,下例中将删除 Point UDT:

DROP TYPE Point

不过这里面有点蹊跷 — 如果 UDT 当前正被使用(如,在一个列的定义中),就好像 Point 示例中那样,该怎么办呢? 很明显,如果其他对象依赖于该类型,您将不能从中删除这一类型。因此必须在删除类型本身之前,确保删除掉了所有使用该类型的对象。这对于删除集同样适用。在删除集之前,必须先删除所有的 UDT。基于最后这个示例,将按照下面的方法来删除 YukonCLR 集:

DROP TABLE PointsDROP TYPE POINTDROP TYPE AddressDROP ASSEMBLY YukonCLR

正如您所看见的,删除其中任意对象的语法是很简单的。当删除一个集时,便从当前的中删除了基本代码,并且如果您想要将它添加回中,必须拥有一个可用的编译版本。

由于受多种因素(例如,存储表示形式、排序和接口)的影响,ALTER TYPE 使用起来并不顺手。如果必须要更改,则一定要删除并重新创建这些对象。不过,ALTER ASSEMBLY 对于修补现有集代码中的错误很有效。

如果您对此有疑惑,这里提供对于在托管 ADO.NET 技术中和非托管客户端 API —(OLE DB、ODBC 以及 ADO)中 UDT 的深层支持。

小结

现在,您了解了如何在 Yukon 和 .NET 中实现用户定义类型以及如何在 Yukon 中使用这些实现。当然,UDT 还有一些其他功能我没有在这里探究,这些内容可能会在以后的 MSDN Magazine 中详解。

相关文章请参见:

" target="_blank">Creating User-Defined Data Types

Peter W. DeBetta 是 Wintellect 公司的教员,同时他还是帮助客户使用 Visual Basic、ASP、C#、ASP.NET 以及 SQL Server 开发企业级软件解决方案的顾问和开发人员。Peter 出版过一些合著书籍,他目前正致力于一本关于 SQL Server“Yukon”的书,该书将由 Microsoft Press 出版。

转载:转载请保留本信息,本文来自http://www.51dibs.com/lp07/la/a24/l_a_d55228039cd578ab.html

客户服务中心信箱:[email protected] [email protected] 网站地图

声明

爱心赞助: