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

来源:互联网 发布:复杂网络研究背景 编辑:程序博客网 时间:2024/05/16 07:23
<script type="text/javascript"><!--google_ad_client = "pub-2947489232296736";/* 728x15, 创建于 08-4-23MSDN */google_ad_slot = "3624277373";google_ad_width = 728;google_ad_height = 15;//--></script><script type="text/javascript"src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>
<script type="text/javascript"><!--google_ad_client = "pub-2947489232296736";/* 160x600, 创建于 08-4-23MSDN */google_ad_slot = "4367022601";google_ad_width = 160;google_ad_height = 600;//--></script><script type="text/javascript"src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>

本文基于MicrosoftSQLServer代号“Yukon”的Beta1版本,文中所有信息都有可能发生变化。

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

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

UDT的值"hspace=4src="/Files/BPic/2006-11/29/06112914201060818.gif"width=7vspace=2border=0>UDT创建程序集"hspace=4src="/Files/BPic/2006-11/29/06112914201060818.gif"width=7vspace=2border=0>UDT"hspace=4src="/Files/BPic/2006-11/29/06112914201060818.gif"width=7vspace=2border=0>UDT以及程序集"hspace=4src="/Files/BPic/2006-11/29/06112914201060818.gif"width=7vspace=2border=0>

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

EXECsp_addtypeZIP,'CHAR(5)','NOTNULL'

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

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

CREATETYPEZIPFROMchar(5)NOTNULL

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

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

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

不透明或抽象类型

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

您可以使用实现UDT协定的托管代码来创建一个类,从而扩展SQL类型系统。接下来使用CREATEASSEMBLY语句将包含UDT的已编译程序集加载到服务器上的数据库中,并使用公开托管代码UDT的CREATETYPE语句在数据库中创建类型。

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

返回页首

创建程序集

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

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

usingSystem.Data.Sql;usingSystem.Data.SqlTypes;

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

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

返回页首

类属性

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


图1类的属性

 

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

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

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

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

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

//[Serializable][SqlUserDefinedType(Format.Native)]'.NET<Serializable(),SqlUserDefinedType(Format.Native)>

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

返回页首

比较UDT的值

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

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

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

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

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

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

返回页首

为空性

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

//C#publicclassMyClass:INullable'VisualBasic.NETPublicClassMyClassImplementsINullable

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

//C#publicboolIsNull{get{returnis_Null;}}'VisualBasic.NETPublicReadOnlyPropertyIsNull()AsBooleanImplementsINullable.IsNullGetReturn(is_Null)EndGetEndProperty

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

返回页首

支持的转换

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

//C#publicstaticMyClassParse(SqlStrings)'VisualBasic.NETPublicSharedFunctionParse(ByValsAsSqlString)AsMyClass

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

//C#publicoverridestringToString()'VisualBasic.NETPublicOverridesFunctionToString()AsString

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

返回页首

其他要求

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

//C#publicMyClass(){}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

返回页首

在SQLServer中使用UDT创建程序集

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

CREATEASSEMBLYASSEMBLY_NAMEFROM'PATH/ASSEMBLY_NAME.dll'

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

CREATEAssemblyYukonCLRFROM'C:/Projects/"Yukon"/UDTs/Point/"Yukon"CLR.dll'

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

返回页首

创建类型

看一下CREATETYPE语句的语法:

CREATETYPE[TYPE_SCHEMA_NAME.]TYPE_NAMEEXTERNALNAMEASSEMBLY_NAME:CLASS_NAME

只要装载了程序集,便可以使用CREATETYPE语句向数据库的可用类型列表中添加类型。与程序集一样,类型仅在当前数据库中创建,而且与程序集一样也被sysname约定所限制。下面的示例将从我刚才展示过的VisualBasic.NETCLR代码中创建PointUDT

CREATETYPEPointEXTERNALNAMEYukonCLR:Point

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

CREATEAssemblyYukonCLRFROM'C:/Projects/Yukon/UDTs/Point/YukonCLR.dll'CREATEAssemblyYukonCLR2FROM'C:/Projects/Yukon/UDTs/Point/YukonCLR2.dll'CREATETYPEPointEXTERNALNAMEYukonCLR:Point--OKCREATETYPEPointEXTERNALNAMEYukonCLR2:Point--BAD-createdinMySchemaCREATETYPEDifferentSchema.PointEXTERNALNAMEYukonCLR2:Point--OK-createdinDifferentSchemaCREATETYPEPointEXTERNALNAMEYukonCLR2:Triangle--OK返回页首

使用UDT

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

CREATETABLEPoints(PointIDintNOTNULL,PntPointNOTNULL)

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

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

PropertyUseSyntax:<implemented_type>::<property_name>=<scalar_expression>MethodUseSyntax:<implemented_type>::<method_name>([arguments])

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

DECLARE@startPointPointDECLARE@endPointPointSET@startPoint=CAST('10:10'ASPoint)SET@endPoint::X=5SET@endPoint::Y=3INSERTPointsVALUES(1,@startPoint)INSERTPointsVALUES(2,@endPoint)

对于任何使用过以前版本SQLServer的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的值:

SELECTPnt::XASXValue,Pnt::YASYValueFROMPoints

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

DECLARE@ptPointSELECT@pt=PntFROMPointsWHEREPointID=2SELECTPnt::DistanceTo(@pt)ASDistanceFROMPointsWHEREPointID=1

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

返回页首

删除和更改UDT以及程序集

使用DROPTYPE语句可以很容易地删除UDT,下例中将删除PointUDT

DROPTYPEPoint

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

DROPTABLEPointsDROPTYPEPOINTDROPTYPEAddressDROPASSEMBLYYukonCLR

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

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

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

返回页首

小结

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

相关文章请参见:

CreatingUser-DefinedDataTypes

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

<script type="text/javascript"><!--google_ad_client = "pub-2947489232296736";/* 728x15, 创建于 08-4-23MSDN */google_ad_slot = "3624277373";google_ad_width = 728;google_ad_height = 15;//--></script><script type="text/javascript"src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>
<script type="text/javascript"><!--google_ad_client = "pub-2947489232296736";/* 160x600, 创建于 08-4-23MSDN */google_ad_slot = "4367022601";google_ad_width = 160;google_ad_height = 600;//--></script><script type="text/javascript"src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>
原创粉丝点击