适用范围:SQL Server
对用户定义类型(UDT)定义进行编码时,必须实现各种功能,具体取决于是将 UDT 实现为类还是结构,以及所选的格式和序列化选项。
本节中的示例演示如何将 Point UDT 实现为 struct(或 Visual Basic 中的 Structure)。
Point UDT 由作为属性过程实现的 X 和 Y 坐标组成。
定义 UDT 时需要下列命名空间:
using System;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
Microsoft.SqlServer.Server 命名空间包含 UDT 的各种属性所需的对象,System.Data.SqlTypes 命名空间包含表示程序集可用的 SQL Server 本机数据类型的类。 程序集可能需要其他命名空间才能正常运行。
Point UDT 还使用 System.Text 命名空间处理字符串。
注意
不支持使用 /clr:pure 编译的可视C++数据库对象(如 UDT)。
指定属性
属性确定如何使用序列化来构造 UDT 的存储表示形式以及如何按值将 UDT 传输到客户端。
Microsoft.SqlServer.Server.SqlUserDefinedTypeAttribute 是必需的。
Serializable 属性是可选的。 还可以指定 Microsoft.SqlServer.Server.SqlFacetAttribute 来提供有关 UDT 的返回类型的信息。 有关详细信息,请参阅 CLR 集成:CLR 例程的自定义属性。
点 UDT 属性
Microsoft.SqlServer.Server.SqlUserDefinedTypeAttribute 将 Point UDT 的存储格式设置为 Native。
IsByteOrdered 设置为 true,这可以保证 SQL Server 中的比较结果与托管代码中的比较相同。 UDT 实现 System.Data.SqlTypes.INullable 接口,以使 UDT 为 null 感知。
以下代码片段显示了 Point UDT 的属性。
[Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedType(Format.Native,
IsByteOrdered=true)]
public struct Point : INullable { ... }
实现可为 null 性
除了为您的程序集正确指定属性外,UDT 还必须支持为 Null 性。 加载到 SQL Server 中的 UDT 具有 null 感知性,但为了使 UDT 能够识别 null 值,UDT 必须实现 System.Data.SqlTypes.INullable 接口。
必须创建一个名为 IsNull的属性,该属性需要确定值是否为 CLR 代码中的 null。 当 SQL Server 找到 UDT 的 null 实例时,UDT 会使用普通的 null 处理方法持久保存。 如果不需要,服务器不会浪费时间序列化或反序列化 UDT,也不会浪费空间来存储 null UDT。 每次从 CLR 中引入 UDT 时,都会对 null 执行此检查,这意味着使用 Transact-SQL IS NULL 构造检查 null UDT 应始终正常工作。 服务器还使用 IsNull 属性来测试实例是否为 null。 一旦服务器确定 UDT 为 Null,它便可以使用其本机 Null 处理方法。
get() 的 IsNull 方法绝不是特殊情况。 如果 Point 变量 @pNull,则默认情况下,@p.IsNull 计算结果为 NULL,而不是 1。 这是因为 SqlMethod(OnNullCall) 方法的 IsNull get() 属性默认为 false。 由于该对象是 Null,因此当请求该属性时,不会反序列化该对象,则不会调用该方法,并返回默认值“NULL”。
示例
在下面的示例中,is_Null 变量是私有的并且保存了 UDT 实例的 Null 状态。 您的代码必须保留 is_Null 的相应值。 UDT 还必须具有一个名为 Null 的静态属性,该属性返回 UDT 的 null 值实例。 这样,如果该实例在数据库中确实为 Null,UDT 便可返回 Null 值。
private bool is_Null;
public bool IsNull
{
get
{
return (is_Null);
}
}
public static Point Null
{
get
{
Point pt = new Point();
pt.is_Null = true;
return pt;
}
}
IS NULL 与 IsNull 的比较
请考虑包含架构 Points(id int, location Point)的表,其中 Point 是 CLR UDT,以及以下查询:
查询 1:
SELECT ID FROM Points WHERE NOT (location IS NULL); -- Or, WHERE location IS NOT NULL;查询 2:
SELECT ID FROM Points WHERE location.IsNull = 0;
这两个查询都返回具有非 null 位置的点 ID。 在 Query 1 中,使用的是正常 Null 处理方法,不需要反序列化 UDT。 另一方面,查询 2 必须反序列化每个非 null 对象并调用 CLR 以获取 IsNull 属性的值。 显然,使用 IS NULL 表现出更好的性能,从 Transact-SQL 代码中读取 UDT 的 IsNull 属性不应该有理由。
那么,IsNull 属性的用法是什么? 首先,需要从 CLR 代码中确定值是否为 null。 其次,服务器需要一种方法来测试实例是否为 null,因此服务器将使用此属性。 确定为 null 后,它可以使用其本机 null 处理来处理它。
实现分析方法
Parse 和 ToString 方法允许从 UDT 的字符串表示形式转换。
Parse 方法允许将字符串转换为 UDT。 它必须声明为 static(或 Visual Basic 中的 Shared),并采用类型为 System.Data.SqlTypes.SqlString的参数。
以下代码实现 Parse UDT 的 Point 方法,该方法分隔了 X 和 Y 坐标。
Parse 方法具有 System.Data.SqlTypes.SqlString类型的单个参数,并假定 X 和 Y 值以逗号分隔的字符串的形式提供。 将 Microsoft.SqlServer.Server.SqlMethodAttribute.OnNullCall 属性设置为 false 可防止从 Point 的 null 实例调用 Parse 方法。
[SqlMethod(OnNullCall = false)]
public static Point Parse(SqlString s)
{
if (s.IsNull)
return Null;
// Parse input string to separate out points.
Point pt = new Point();
string[] xy = s.Value.Split(",".ToCharArray());
pt.X = Int32.Parse(xy[0]);
pt.Y = Int32.Parse(xy[1]);
return pt;
}
实现 ToString 方法
ToString 方法将 Point UDT 转换为字符串值。 在这种情况下,将为 Point 类型的 Null 实例返回字符串“NULL”。
ToString 方法通过使用 Parse 返回由 X 和 Y 坐标值组成的逗号分隔 System.Text.StringBuilder 来反转 System.String 方法。 由于 InvokeIfReceiverIsNull 默认为 false,因此不需要检查 Point 的 null 实例。
private Int32 _x;
private Int32 _y;
public override string ToString()
{
if (this.IsNull)
return "NULL";
else
{
StringBuilder builder = new StringBuilder();
builder.Append(_x);
builder.Append(",");
builder.Append(_y);
return builder.ToString();
}
}
公开 UDT 属性
Point UDT 公开作为 System.Int32类型的公共读写属性实现的 X 和 Y 坐标。
public Int32 X
{
get
{
return this._x;
}
set
{
_x = value;
}
}
public Int32 Y
{
get
{
return this._y;
}
set
{
_y = value;
}
}
验证 UDT 值
使用 UDT 数据时,SQL Server 数据库引擎会自动将二进制值转换为 UDT 值。 该转换过程包括检查值是否符合类型的序列化格式和确保可以正确地反序列化值。 这可确保可以将值转换回二进制形式。 对于采用字节顺序的 UDT,这样还可以确保产生的二进制值与原始二进制值匹配。 这会防止在数据库中保留无效值。 在某些情况下,这种检查级别可能不足。 当需要 UDT 值处于预期域或范围内时,可能需要进行额外的验证。 例如,实现日期的 UDT 可能要求日值为特定的有效值范围内的正数。
Microsoft.SqlServer.Server.SqlUserDefinedTypeAttribute.ValidationMethodName 的 Microsoft.SqlServer.Server.SqlUserDefinedTypeAttribute 属性允许你提供在将数据分配给 UDT 或转换为 UDT 时服务器运行的验证方法的名称。 在运行 ValidationMethodName 实用工具、、BULK INSERT、DBCC CHECKDB、DBCC CHECKFILEGROUP、分布式查询和表格数据流(TDS)远程过程调用(RPC)操作期间,也会调用 DBCC CHECKTABLE。
ValidationMethodName 的默认值为 null,表示没有验证方法。
示例
以下代码片段显示了 Point 类的声明,该声明指定 ValidationMethodName的 ValidatePoint。
[Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedType(Format.Native,
IsByteOrdered=true,
ValidationMethodName = "ValidatePoint")]
public struct Point : INullable { ... }
如果指定了验证方法,则必须具有如下面的代码段所示的签名。
private bool ValidationFunction()
{
if (validation logic here)
{
return true;
}
else
{
return false;
}
}
验证方法可以具有任何范围,如果值有效,则应返回 true,否则 false。 如果方法返回 false 或引发异常,则该值被视为无效,并引发错误。
在以下示例中,代码仅允许 X 和 Y 坐标的值为零或更大。
private bool ValidatePoint()
{
if ((_x >= 0) && (_y >= 0))
{
return true;
}
else
{
return false;
}
}
验证方法限制
当服务器执行转换时,服务器将调用验证方法,而不是当通过设置单个属性或使用 Transact-SQL INSERT 语句插入数据时插入数据时调用验证方法。
如果希望验证方法在所有情况下执行,则必须从属性 setter 和 Parse 方法显式调用验证方法。 这不是一项要求,在某些情况下甚至可能不需要。
分析验证示例
若要确保在 ValidatePoint 类中调用 Point 方法,必须从 Parse 方法和设置 X 和 Y 坐标值的属性过程调用该方法。 以下代码片段演示如何从 ValidatePoint 函数调用 Parse 验证方法。
[SqlMethod(OnNullCall = false)]
public static Point Parse(SqlString s)
{
if (s.IsNull)
return Null;
// Parse input string to separate out points.
Point pt = new Point();
string[] xy = s.Value.Split(",".ToCharArray());
pt.X = Int32.Parse(xy[0]);
pt.Y = Int32.Parse(xy[1]);
// Call ValidatePoint to enforce validation
// for string conversions.
if (!pt.ValidatePoint())
throw new ArgumentException("Invalid XY coordinate values.");
return pt;
}
属性验证示例
以下代码片段演示如何从设置 X 和 Y 坐标的属性过程调用 ValidatePoint 验证方法。
public Int32 X
{
get
{
return this._x;
}
// Call ValidatePoint to ensure valid range of Point values.
set
{
Int32 temp = _x;
_x = value;
if (!ValidatePoint())
{
_x = temp;
throw new ArgumentException("Invalid X coordinate value.");
}
}
}
public Int32 Y
{
get
{
return this._y;
}
set
{
Int32 temp = _y;
_y = value;
if (!ValidatePoint())
{
_y = temp;
throw new ArgumentException("Invalid Y coordinate value.");
}
}
}
代码 UDT 方法
编写 UDT 方法时,请考虑所用的算法是否可能会随时间的推移而改变。 如果是这样,你可能需要考虑为 UDT 使用的方法创建单独的类。 如果算法发生更改,则可以使用新代码重新编译类,并将程序集加载到 SQL Server 中,而不会影响 UDT。 在许多情况下,可以使用 Transact-SQL ALTER ASSEMBLY 语句重新加载 UDT,但这可能会导致现有数据出现问题。 例如,Currency 示例数据库随附的 AdventureWorks2025 UDT 使用 ConvertCurrency 函数转换货币值,该函数在单独的类中实现。 转换算法将来可能会以不可预知的方式变化,或者可能需要新功能。 规划将来更改时,将 ConvertCurrency 函数与 Currency UDT 实现分离可提供更大的灵活性。
示例
Point 类包含三种用于计算距离的简单方法:Distance、DistanceFrom和 DistanceFromXY。 每个返回一个 double 计算从 Point 到零的距离、从指定点到 Point的距离,以及从指定的 X 和 Y 坐标到 Point的距离。
Distance 和 DistanceFrom 每个调用 DistanceFromXY,并演示如何对每个方法使用不同的参数。
// Distance from 0 to Point.
[SqlMethod(OnNullCall = false)]
public Double Distance()
{
return DistanceFromXY(0, 0);
}
// Distance from Point to the specified point.
[SqlMethod(OnNullCall = false)]
public Double DistanceFrom(Point pFrom)
{
return DistanceFromXY(pFrom.X, pFrom.Y);
}
// Distance from Point to the specified x and y values.
[SqlMethod(OnNullCall = false)]
public Double DistanceFromXY(Int32 iX, Int32 iY)
{
return Math.Sqrt(Math.Pow(iX - _x, 2.0) + Math.Pow(iY - _y, 2.0));
}
使用 SqlMethod 属性
Microsoft.SqlServer.Server.SqlMethodAttribute 类提供自定义属性,可用于标记方法定义,以便指定确定性、对 null 调用行为,以及指定方法是否为变量。 使用的是这些属性的默认值,仅在需要非默认值时才使用自定义特性。
注意
SqlMethodAttribute 类继承自 SqlFunctionAttribute 类,因此 SqlMethodAttribute 继承 FillRowMethodName 和 TableDefinitionSqlFunctionAttribute字段。 这意味着可以编写表值方法,但情况并非如此。 该方法编译和程序集部署,但在运行时引发有关 IEnumerable 返回类型的错误,并显示以下消息:“程序集 <name> 中类 <class> 中的方法、属性或字段 <assembly> 具有无效的返回类型。
下表介绍了一些可在 UDT 方法中使用的相关 Microsoft.SqlServer.Server.SqlMethodAttribute 属性,并列出其默认值。
| 财产 | 描述 |
|---|---|
DataAccess |
指示函数是否涉及访问存储在 SQL Server 本地实例中的用户数据。 默认值为 DataAccessKind.None。 |
IsDeterministic |
指示在输入值相同且数据库状态相同的情况下函数是否会生成相同的输出值。 默认值为 false。 |
IsMutator |
指示方法是否在 UDT 实例中引起状态变化。 默认值为 false。 |
IsPrecise |
指示函数是否涉及不精确的计算,如浮点运算。 默认值为 false。 |
OnNullCall |
指示在指定 Null 引用输入参数时是否调用此方法。 默认值为 true。 |
示例
使用 Microsoft.SqlServer.Server.SqlMethodAttribute.IsMutator 属性可以标记允许更改 UDT 实例状态的方法。 Transact-SQL 不允许在一个 SET 语句的 UPDATE 子句中设置两个 UDT 属性。 然而,您可以将一个方法标记为更改两个成员的赋值函数。
注意
查询中不允许使用 Mutator 方法。 这些方法只能在赋值语句或数据修改语句中调用。 如果标记为 mutator 的方法未返回 void(或不是 Visual Basic 中的 Sub),则 CREATE TYPE 失败并出现错误。
以下语句假定存在具有 Triangles 方法的 Rotate UDT。 以下 Transact-SQL update 语句调用 Rotate 方法:
UPDATE Triangles
SET t.RotateY(0.6)
WHERE id = 5;
Rotate 方法使用 SqlMethod 属性设置 IsMutator 修饰为 true,以便 SQL Server 可以将该方法标记为 mutator 方法。 该代码还会将 OnNullCall 设置为 false,这指示如果任何输入参数为空引用,该方法将返回 null 引用(Nothing 在 Visual Basic 中)。
[SqlMethod(IsMutator = true, OnNullCall = false)]
public void Rotate(double anglex, double angley, double anglez)
{
RotateX(anglex);
RotateY(angley);
RotateZ(anglez);
}
使用用户定义的格式实现 UDT
使用用户定义的格式实现 UDT 时,必须实现 Read 和 Write 方法,实现 Microsoft.SqlServer.Server.IBinarySerialize 接口来处理序列化和反序列化 UDT 数据。 还必须指定 MaxByteSize的 Microsoft.SqlServer.Server.SqlUserDefinedTypeAttribute 属性。
货币 UDT
Currency UDT 包含在可以使用 SQL Server 安装的 CLR 示例中。
Currency UDT 支持处理特定文化的货币系统中的货币金额。 必须定义两个字段:string的 CultureInfo,该字段指定颁发货币(例如en-us)和 decimalCurrencyValue(货币金额)。
尽管服务器不使用它来执行比较,但 Currency UDT 实现 System.IComparable 接口,该接口公开单个方法 System.IComparable.CompareTo。 在需要准确比较或对区域性中的货币值进行排序的情况下,这在客户端使用。
在 CLR 中运行的代码将区域性与货币值分开比较。 对于 Transact-SQL 代码,以下操作确定比较:
将
IsByteOrdered属性设置为 true,该属性指示 SQL Server 使用磁盘上的持久二进制表示形式进行比较。使用
WriteUDT 的Currency方法来确定 UDT 如何在磁盘上持久保存,因此如何比较和排序 UDT 值,以便进行 Transact-SQL 操作。使用以下二进制格式保存
CurrencyUDT:将区域性另存为 UTF-16 编码的字符串并用第 0-19 个字节表示,右侧以 Null 字符填充。
使用第 20 个字节及以后的字节包含货币的十进制值。
填充的目的是确保区域性与货币值完全分离,以便当一个 UDT 与 Transact-SQL 代码中的另一个 UDT 进行比较时,将区域性字节与区域性字节进行比较,而货币字节值与货币字节值进行比较。
货币属性
Currency UDT 是使用以下属性定义的。
[Serializable]
[SqlUserDefinedType(Format.UserDefined,
IsByteOrdered = true, MaxByteSize = 32)]
[CLSCompliant(false)]
public struct Currency : INullable, IComparable, IBinarySerialize
{ ... }
使用 ibinaryserialize 创建读取和写入方法
选择 UserDefined 序列化格式时,还必须实现 IBinarySerialize 接口并创建自己的 Read 和 Write 方法。
Currency UDT 中的以下过程使用 System.IO.BinaryReader 和 System.IO.BinaryWriter 从 UDT 读取和写入 UDT。
// IBinarySerialize methods
// The binary layout is as follow:
// Bytes 0 - 19:Culture name, padded to the right
// with null characters, UTF-16 encoded
// Bytes 20+:Decimal value of money
// If the culture name is empty, the currency is null.
public void Write(System.IO.BinaryWriter w)
{
if (this.IsNull)
{
w.Write(nullMarker);
w.Write((decimal)0);
return;
}
if (cultureName.Length > cultureNameMaxSize)
{
throw new ApplicationException(string.Format(
CultureInfo.InvariantCulture,
"{0} is an invalid culture name for currency as it is too long.",
cultureNameMaxSize));
}
String paddedName = cultureName.PadRight(cultureNameMaxSize, '\0');
for (int i = 0; i < cultureNameMaxSize; i++)
{
w.Write(paddedName[i]);
}
// Normalize decimal value to two places
currencyValue = Decimal.Floor(currencyValue * 100) / 100;
w.Write(currencyValue);
}
public void Read(System.IO.BinaryReader r)
{
char[] name = r.ReadChars(cultureNameMaxSize);
int stringEnd = Array.IndexOf(name, '\0');
if (stringEnd == 0)
{
cultureName = null;
return;
}
cultureName = new String(name, 0, stringEnd);
currencyValue = r.ReadDecimal();
}