65.9K
CodeProject 正在变化。 阅读更多。
Home

SqlTimeSpan

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2009年7月23日

CPOL

19分钟阅读

viewsIcon

47791

downloadIcon

176

将 System.TimeSpan 移植为 SQL Server UDT。

引言

本文介绍了一个自定义的 SQL Server 用户定义类型 (UDT),用于存储时间间隔以及与之相关的其他工件。它是 Base Class Library 中的 System.TimeSpan 结构的一个包装器,这意味着大多数实现都是通过调用该结构实现的。大部分讨论集中在 SQL 语言的局限性以及与 SQL Server 内部机制的兼容性所带来的调整。其中,二进制序列化因其复杂性和对 UDT 行为的深刻影响而需要专门讨论。配套的工件包括一个聚合函数、函数和扩展方法。由于它们依赖于 UDT 的方法,因此实现起来很简单,所以本文不再深入探讨。

这不是一个关于 UDT 的教程,我希望您已经对 .NET 和数据库的基础知识有了深刻的理解。如果您需要对 UDT 有透彻的理解,我将推荐您阅读我写的另一篇文章,您可以在此处找到。尽管本文中的部分代码受到 UDT 如何最终用于前端应用程序的影响,但关于如何在 SQL Server 之外使用它的讨论超出了本文的范围。

UDT 构造

公共接口

我们开始剖析 UDT 代码,从样板代码开始。它们与此处所示的一样直接。

[Serializable]
[SqlUserDefinedType(Format.UserDefined,
                    MaxByteSize = 9,
                    IsByteOrdered = true,
                    IsFixedLength = false)]
public struct SqlTimeSpan : INullable, IBinarySerialize
{
    #region Mandatory Members

    internal System.TimeSpan SystemTimeSpan;

    public override string ToString()
    {
        return SystemTimeSpan.ToString();
    }

    public static SqlTimeSpan Parse(SqlString input)
    {
        SqlTimeSpan udt = new SqlTimeSpan();
        udt.SystemTimeSpan = TimeSpan.Parse(input.Value);
        return udt;
    }

    bool _isNull;
    public bool IsNull
    {
        [SqlMethod(InvokeIfReceiverIsNull = true)]
        get { return _isNull; }
    }

    public static SqlTimeSpan Null
    {
        get
        {
            SqlTimeSpan udt = new SqlTimeSpan();
            udt._isNull = true;
            return udt;
        }
    }

    #endregion
    
    // Rest of the codes
}

我们只是将工作委托给底层的 TimeSpan 结构。它被限定为 internal,以便于程序集内的某些方法和对象访问。IsNullget 方法上的装饰确保了在 UDT 为 SQL-null 时查询此属性返回 false 而不是“null”。(从现在开始,我将使用 *SQL-null* 来指代 SQL 三值逻辑中固有的 null。这与 .NET 上下文中的 null 不同,后者是指向空内存的引用。)

System.TimeSpan 对象是 SQL Server 在您协助之前无法序列化的对象之一。这需要我们在属性中使用用户定义的格式并实现 IBinarySerialize 接口。

除了格式,我们还为一些属性指定了值。大小 9 足够存储 TimeSpanTicks 属性的 8 字节 System.UInt64(C# 中的 ulong)等效值以及 SQL nullability 标志。UDT 是*按字节排序*的,这意味着 SQL Server 依赖于磁盘上存储的位进行排序。最后,我们告诉 SQL Server 我们的 UDT 是可变长度的,这样我们就无需为 SQL-null UDT 使用额外的 8 字节。当我们将要处理 IBinarySerialize 的实现时,将会有更多关于这些属性的说明。

由于这个 UDT 只是 TimeSpan 的包装器,因此外部应用程序使用底层字段更有意义。我们有一个返回该字段的方法,但仅在 SQL 外部调用时有效。原因再明显不过了。

public TimeSpan GetSystemTimeSpan()
{
    // We ensure this is called only outside SQL Server
    // because TimeSpan is not recognized there
    if (SqlContext.IsAvailable)
        throw new NotSupportedException("GetSystemTimeSpan " + 
                  "is not supported inside SQL Server.");

    return SystemTimeSpan;
}

SQL 不支持构造函数调用。当我们为 UDT 赋值一个字符串值时,就会模拟构造函数调用,该调用会内部调用 Parse。由于参数是字符串,Parse 非常容易出现拼写错误。如果我们提供另一种使用强类型参数初始化 UDT 的方法,那就更好了,比如这样:

static public SqlTimeSpan CreateSqlTimeSpan(int days, int hours, int minutes, 
                          int seconds, int milliseconds)
{                                            
    System.TimeSpan ts = new System.TimeSpan(days, hours, minutes, 
                             seconds, milliseconds);
    SqlTimeSpan tsUdt = new SqlTimeSpan(ts, false); 
    return tsUdt;
}

接下来,我们提供一个构造函数来初始化我们的字段。这应该只在第二个参数有意义的程序集内部调用。

// SQL-nullability and the fact that our type
// is a struct necessitate this constructor
internal SqlTimeSpan(System.TimeSpan ts, bool isNull)
{
    SystemTimeSpan = ts;
    _isNull = isNull;
}

UDT 中允许调用的方法取决于您使用的 SQL 语句。只有*修改器*(VB 中的*setter*和 Sub)允许在 UPDATE 语句和 SET 中使用,而只有*查询*(VB 中的*getter*和 Function)允许在 SELECT 语句和 PRINT 中使用。是否提供方法的两个版本取决于程序员的决定。在我们的 UDT 中,修改器-查询对的一个例子由 Negate - GetNegated 说明。

[SqlMethod(IsMutator = true)]
public void Negate()
{
    SystemTimeSpan = SystemTimeSpan.Negate();
}

public SqlTimeSpan GetNegated()
{
    return new SqlTimeSpan(SystemTimeSpan.Negate(), false);
}

我们实际上可以只使用查询版本。我们仍然可以通过它来更新我们的 UDT,尽管不是最高效的方式。理想情况下,如果您调用一个修改器方法,它内部所做的只是更改至少一个字段的值。使用查询版本更新列会不必要地创建对象的另一个实例,如 GetNegated 所示。如果您的 UDT 是原生格式,这可能不是问题,但对于用户定义的类型和数千行需要更新的行,这会影响性能。坚持使用修改器方法进行更新的另一个优点是代码的简洁性,如下面的示例所示。

-- Assume tsCol is SqlTimeSpan
 
-- Using mutator method
-- Significantly shorter and more object-oriented
UPDATE TimeSpanTable SET tsCol.Negate()

-- Using query method
-- Note we’re assigning new instance
UPDATE TimeSpanTable SET tsCol = tsCol.GetNegated()

T-SQL 不支持运算符和重载方法。这对您创建 UDT 的方法有重大影响。您必须权衡成对方法的好处与缺点——一个臃肿的对象。幸运的是,我们的 UDT 是按字节排序的,这意味着相等运算符立即可用。这些运算符对于我们 UDT 的*可排序性*是必需的,但正如您稍后将看到的,除非我们在序列化过程中操作位,否则它们不会产生正确的结果。这项艰巨的工作通常会让开发人员望而却步,他们只会满足于非按字节排序的格式,并实现符号相等运算符的名义等效项。如果我们走了这条路,我们只会实现类似这样的东西。

// No need for this verbosity if UDT is byte-ordered
static public bool Equals(SqlTimeSpan ts1, SqlTimeSpan ts2) 
{
    return TimeSpan.Equals(ts1.SystemTimeSpan, ts2.SystemTimeSpan);    
}

在涉及细粒度单位或浮点数的 UDT 中,一件重要的事情是,两个 UDT 很难在最后一个小数位上完全相等。拥有一个允许一定范围作为容差的宽松*等于*运算符会很好。我们的 UDT 通过 ApproxEquals 运算符满足了这一需求。它类似于 Equals,只是它接受另一个时间间隔,指定操作数之间的差异被认为可以忽略的范围。

static public bool ApproxEquals(SqlTimeSpan ts1, 
              SqlTimeSpan ts2, SqlTimeSpan allowedMargin)
{
    TimeSpan diff = ts1.SystemTimeSpan - ts2.SystemTimeSpan;  
    return (diff.Duration() <= allowedMargin.SystemTimeSpan.Duration());
}

关于剩余的属性和方法,它们只是镜像了其 TimeSpan 对等项的实现,没有什么可讨论的。但有一点需要注意,其中一些属性带有 SqlMethodAttribute.IsDeterministic 装饰。我们应该这样做,以允许对这些属性进行索引。基本上,它的意思是这些属性的值可以根据输入值进行预测;在这种情况下,是任何我们用来定义 TimeSpan 字段的值。这里是那些确定性属性之一。

public double TotalDays
{   
    [SqlMethod(IsDeterministic = true)]  
    get { return SystemTimeSpan.TotalDays; }
}

我们的 UDT 几乎完成了。我们只需要将其持久化到磁盘。这是一个手动过程,您最好做好准备,因为它会有点棘手。

二进制序列化

只有当字段类型是*可进行位操作*时,SQL Server 才能自行将 UDT 保存和检索到磁盘。 可进行位操作的类型在托管和非托管环境中具有相同的表示形式。我们的 TimeSpan 肯定不是其中之一,这就是为什么我们将“用户定义”指定为我们的 UDT 格式的原因。因此,我们还必须实现 IBinarySerialize 接口。实现的复杂性取决于您希望*标量*UDT 最终包含的功能。如果您希望它直接响应 SQL 运算符,参与索引,并*可排序*,那么您必须采用“手动”方法,我将稍后讨论。另一方面,如果这些功能对您不重要,那么您可以使用这些简单的几行来选择“快速”方法。

public void Read(System.IO.BinaryReader r)
{
    _isNull = r.ReadBoolean();
    if (!IsNull)
        SystemTimeSpan = TimeSpan.FromTicks(r.ReadInt64());
}
        
public void Write(System.IO.BinaryWriter w)
{   
    w.Write(IsNull);
    if (!IsNull)
        w.Write(Ticks);
}

不要让上面代码的简洁性欺骗了您。您仍然可以像手动实现一样对待您的 UDT。诀窍是公开一个可以表示 UDT 的属性。该属性的数据类型应该是原生的,这样 SQL Server 在处理它时就不会有问题。这只有在您的 UDT 确实是*标量*时才可能。因为标量 UDT 可以展平为一个单元而不会丢失其值。就时间间隔而言,这个单元是通过 Ticks 来提供的。应用排序和其他运算符只是调用此属性的问题。索引可能会占用一些空间,因为您必须将属性持久化为列。SQL Server 不允许索引属性。

-- Creating index from a persisted UDT property
-- Property must be deterministic
CREATE TABLE t (tsCol SqlTimeSpan, Ticks AS tsCol.Ticks PERSISTED)
GO
CREATE CLUSTERED INDEX IX_T ON t (Ticks)
GO

除了功能之外,两种实现之间的另一个有趣的比较点是效率。我检查了对 5000 行进行排序时两种实现的执行计划。调用 Ticks 属性涉及另一个步骤,但其影响非常小,所以我认为它并不重要。令我惊讶的是总执行时间,我曾预计它会毫无悬念地偏向快速实现。然而,快速实现花了 0.280 秒,而手动实现只花了 0.241 秒。在*估计行大小*方面,快速实现也有显着膨胀,从 15 字节增加到 23 字节,从*计算标量*步骤开始。手动实现一直保持 15 字节的估计。增加的大小一定是由于调用 Ticks 属性,该属性需要额外的字节来存储。*缓存计划大小*也偏向手动,只有 8 字节,是快速实现的一半。唯一有利于快速实现的统计数据是*表扫描*期间的*估计 I/O 成本*,这在意料之中,但仍然非常微不足道。但是,请记住,这些统计数据只是估计值,我已经读过很多声称它们有时不一致、具有误导性或完全错误的说法。我仍然鼓励您进行自己的测试来进一步证明编码。如果您不相信手动实现序列化的好处,那么您可以直接跳到配套工件部分。

Quick VS Manual

手动序列化的第一步是确保 SqlUserDefinedAttribute.IsByteOrdered 设置为 true。这告诉 SQL Server 它可以将我们 UDT 的持久化字节用作比较的依据。为了让事情更简单一些,我们只持久化 Ticks 属性,但有一个问题。Ticks 是 long (System.Int64),而 .NET 持久化有符号数值类型的方式很奇怪。负值实际上会作为大于类型最大值的值持久化,并且是降序的。为了更好地说明这一点,我在源代码中包含了一个名为 LongUDT 的朴素 UDT 来探讨这种行为。让我们看看 SQL Server 如何使用 LongUDT 来处理这些值。

Long UDT sort

我们按 UDT 列排序时得到混乱结果的原因很明显。SQL Server 依赖于存储的值,这些值与其表示值相差甚远。例如,long 的最小值实际上存储为 128,而 1 存储为 72,057,594,037,927,936。

我试图通过偏移值并将它们存储为 ulong 来欺骗 SQL Server。如果我将 *long.MaxValue* + 1(即 (2 的 64 次方 / 2) + 1)加到任何 long 值,我就能确保我的 ulong 值是正数。然而,应用偏移量会产生一个更令人费解的结果。

LongUDT offsetted sort

ulong.MinValue(第一行)存储正确,但其他值都偏离得非常远。零,我期望它只是偏移值,存储为 128,而 long.MinValue + 1(第三行),我期望它是 1,存储为 72,057,594,037,927,936。摆弄所有这些位并不是我的强项,但我只是需要在 SQL Server 之外进行调查,所以我做了。结果与我的预测完全一致。

Console app showing hex

上面的控制台清楚地表明,当这些位被转换为字节时,它们会以某种方式混乱。有了这个新发现,如果我们能够在 SQL Server 持久化这些位时保持其位排列,我们应该就能正确地对表进行排序。

在我们的 UDT 中,Read 和 Write 的任务是偏移 Ticks 值,并确保将正确的位复制到磁盘和从磁盘读取。它们依赖于执行位上脏活的静态辅助方法。除了这些方法之外,还有一些辅助属性有助于代码的可读性。所有这些都是自解释的,除了 IsCircularOnBit32。我们使用它来检查*循环位移*,这可能对我们的 UDT 产生毁灭性的影响。想象一下,如果您得到 1 而不是 4,294,967,296,结果会怎样!下面是 IBinarySerialize.Write 的实现。

readonly static bool IsCircularOnBit32;
readonly static long TicksOffset = long.MaxValue;
readonly static int ByteLength = 8;
readonly static int UdtLength = 72; // 9 * ByteLength
                                    // 1 byte for nullability
                                    // 8 bytes for the Udt
static SqlTimeSpan()
{
    IsCircularOnBit32 = (1U == 1U << 32);
}    

public void Write(System.IO.BinaryWriter w)
{
    if (!IsNull)
    {
        // We write 9 bytes, 1st write is on the 1st bit of 1st byte
        // and  subseqent writes are by 8-bit chunk for the ticks
        byte[] targetBytes = new byte[9];
        targetBytes[0] = 0;
        ulong sourceValue = (ulong)(Ticks + TicksOffset) + 1;
        int sourceStartBit = UdtLength - 1;
        for (int i = 1; i < targetBytes.Length; i++)
        {
            // We move by byte-length to the right
            sourceStartBit -= ByteLength;
            ULongToByte(sourceValue, sourceStartBit, ref targetBytes[i]);
        }
        w.Write(targetBytes);
    }
    else
    {
        // It's null so only 1 byte is needed
        byte isNullByteFlag = 1;
        w.Write(isNullByteFlag);
    }
}

代码块首先检查 UDT 是否不是 SQL-Null。如果是 SQL-Null,则无需进一步操作。有效地,如果 UDT 是 SQL-Null,我们只使用 1 字节。这就是为什么我们在 UDT 装饰中将 SqlUserDefinedTypeAttribute.IsFixedLength 指定为 false 的原因。

此方法的工作核心是 ULongToByte,我们稍后将讨论它。基本上,它的作用是以 8 位为增量将位从 ulong 值复制到字节值。复制涉及基本的位操作,您可能想在这里刷新一下自己

对于非 SQL-Null UDT,我们使用 9 字节:1 个用于 SQL-nullability,8 个用于 Ticks 的偏移 ulong 值。首先,我们对数组的第一个字节执行类似的写入,但这次将其关闭以指示*非* SQL-Null。然后是 Ticks 值的偏移。新的 ulong 值将从第 63 位到第 0 位,以 8 位为增量进行遍历。UDT 的长度是 9 字节或 72 位,但由于我们已经为 SQL-nullability 使用了第 71 位到第 64 位,所以我们应该从第 63 位开始读取。下表应该让您清楚每次迭代处理的位数。

ULongToByte iteration table

当然,读取只是相反的过程。我们遍历持久化字节数组中的每个字节,并将位复制到 ulong 变量。整个代码块如下所示。

public void Read(System.IO.BinaryReader r)
{
    // We check if the bytes stored
    // indicates a null UDT. 
    byte isNullByteFlag = r.ReadByte();
    if (isNullByteFlag == 0)
    {
        // Since it's not null, we are assured
        // of additional 8 bytes. We copy the bits
        // to a ulong and ultimately to our ticks

        _isNull = false;
        ulong targetValue = 0;
        byte[] sourceBytes = r.ReadBytes(8);
        int targetStartBit = UdtLength - 1;
        for (int i = 0; i < sourceBytes.Length; i++)
        {
            targetStartBit -= ByteLength;
            ByteToULong(sourceBytes[i], ref targetValue, targetStartBit);
        }
        long correctedValue = 
            (long)(targetValue - (ulong)TicksOffset) - 1;
        SystemTimeSpan = TimeSpan.FromTicks(correctedValue);
    }
    else
        _isNull = true; // This is all we need to do
}

它从读取第一个字节开始,这是 SQL-nullability 标志。请注意,读取顺序与写入顺序相同。如果是 SQL-null,我们就不 bother 读取剩余的 8 个字节。如果不是,我们就遍历字节数组,每次写入 8 位到一个 ulong 累加器变量。它是一个累加器,因为我们将它的值传递给下一次调用。当我们稍后详细讨论 ByteToULong 方法时,这一点将变得清楚。累加后,我们偏移以获取 Ticks 的原始 long 值。下表显示了每次迭代处理的位。

ByteToULong iteration table

ByteToULong 方法负责将位复制到内存中的正确位置。它由四个步骤组成:

  • 创建适当的掩码
  • 从源中删除非相关位
  • 对齐源和目标位
  • 将位复制到目标

这个过程相当标准,但对于不经常进行此类操作的人来说可能令人生畏。不要担心,当我们使用一个具体的例子来详细介绍时,一切都会清楚。

static void ULongToByte(ulong source, int sourceStartBit, ref byte target)
{
    // 1. Set masks
    ulong copyMask = 1U << (sourceStartBit + 1);
    copyMask -= 1;
    int bitLengthToCopy = ByteLength;
    ulong dropMask = 1U << ((sourceStartBit + 1) - bitLengthToCopy);
    dropMask -= 1;

    // 2. Drop non-pertinent bits from the target
    dropMask ^= 0xFFFFFFFFFFFFFFFF;
    copyMask &= dropMask;
    // Correction for circular bit shift 
    // on non-64-bit machines
    if ((sourceStartBit >= 31) && IsCircularOnBit32)
        source ^= copyMask;
    else
        source &= copyMask;

    // 3. Align source bits with the target bits
    int targetStartBit = ByteLength - 1;
    int shift = sourceStartBit - targetStartBit;
    source >>= shift;

    // 4. Copy
    ulong byteMask = 1U << ByteLength;
    byteMask -= 1;
    target |= (byte)(source & byteMask);
}

让我们使用 Ticks 值为 8,589,942,784 的示例来逐步分析代码。偏移后,这将变为 9,223,372,045,444,718,592。这确实是一个很大的值,但它的位表示足以说明我们在检查循环移位后所采取的每项操作。对于第一次演练,我们将说明源起始位小于 31 的情况。这意味着我们已经进入了第 7 次迭代。我们的最终目标是从下面突出显示的八位字节中提取它,并将其放入目标字节变量。

第一步是创建适当的复制和删除掩码。源起始位是第 15 位(第 16 位),应用移位后,我们得到以下结果:

Step 1

接下来,我们从复制掩码中删除非相关位。目标是得到一个只有第二八位字节为 1,其余为 0 的掩码。(为清晰起见,后续的说明将突出显示任何位值,这些值是位运算的结果。)

Step 2

现在我们已经有了复制掩码来隔离相关位,下一步是将其应用于我们的源。我的机器是 32 位,这意味着执行 AND 位运算。我们应该得到一个结果,只保留相关八位字节中的 1。

Step 3

然而,上面的结果不能立即进行掩码,因为它将产生一个大于字节可以存储的值。我们应该将其对齐到第一个八位字节,使其最大值为 255——这是 byte 数据类型的最大值。“对齐”实际上是一个误称,因为我们真正做的是通过向右移位来减小值。

Step 4

最后,我们可以对源进行掩码以提取每次迭代所需的值。

Step 4

下一个情况发生在我们处于第 4 次迭代(从右边数的第 5 个八位字节)时。这说明了当源起始位大于 31 且代码在 32 位机器上运行时所采取的操作。第 34 位是 1,这在目标字节中转化为字节值 2。您现在应该熟悉所涉及的所有操作,因此无需再进行演练。请注意,复制掩码最终为 255,而不是预期的 4,294,967,295。这是循环位移的效果。用于纠正这一点的独特操作是蓝色面上的那个。

Step 5

保存工作到此结束。另一方面,读取由另一个辅助方法 ByteToULong 处理。如果保存很复杂,那么这个就很简单了。原因是我们的目标数据类型可以容纳比源中更多的位。我们只需要做的是将源八位字节对齐到目标八位字节,然后执行位 OR 操作。如我之前所说,我们使用位 OR,因为我们只是为目标(我之前称为*累加器*)添加更多值。这是实现。

static void ByteToULong(byte source, ref ulong target, int targetStartBit)
{
    ulong maskableSource = (ulong)source;

    // 1.  Align source bits with the target bits
    int sourceStartBit = ByteLength - 1;
    int shift = targetStartBit - sourceStartBit;
    maskableSource <<= shift;

    // 2. Copy
    target |= maskableSource;
}

例如,假设我们已经进入了第 4 次迭代。在此迭代中,我们的目标是将字节值 2 放置到八位字节 5。步骤如下所示。

Byte to ulong example

请注意,剩余的 8,192 将由第 7 次迭代处理。在该迭代中,值 32 将左移 8 位,从而产生产生原始 ulong 值所需的正确值。您可以通过运行以下语句来验证整个演练。

Verifying example

我们有 18 位十六进制值,因为一个字节由 2 个十六进制数字表示。左侧的前两个零对应于 SQL-nullability 标志。下面的分解显示了我们的 ulong(已偏移)值如何存储。

2 x (16 pow 3)  =                     8,192 
2 x (16 pow 8)  =             8,589,934,592
8 x (16 pow 15) = 9,223,372,036,854,775,808
          Total = 9,223,372,045,444,718,592

有了所有二进制序列化例程就位后,运行前面的语句应该会给我们预期的结果。

Correct sort

您可以看到偏移策略是有效的。最小的 Ticks 值存储为 0,0 存储为偏移量加 1,依此类推。而且,既然您现在确信您的 UDT 可以正确排序,您可能还想看看这个。

UDT as PK

那么,为什么要在文章中费力地阅读所有这些内容呢?我只是觉得我欠读者一个解释,揭开 UDT 编程中最晦涩但又至关重要的方面。如果您能够控制位在磁盘上的布局方式,您在数据类型方面就不再受限制。在存储和性能优化方面,您可以几乎做到一切。最重要的是,您现在拥有一个真正响应关系语义的 UDT。

配套工件

Aggregate

如果您想获得由我们的 UDT 定义的列的总和,您可以使用现有 SUM() 聚合函数的 Ticks 属性。但这并不直接,因为您必须将结果重新组装成 SqlTimeSpan 以便正确显示。避免这种情况并不是创建 UDT 专用聚合函数的有力理由,但如果您有兴趣,您的代码可能如下所示。

[SqlUserDefinedAggregate(Format.UserDefined, MaxByteSize = 10
    , IsInvariantToDuplicates = false
    , IsInvariantToNulls = true
    , IsInvariantToOrder = true)]
public struct SumTS : IBinarySerialize{
    SqlTimeSpan _accumulatedTS;
    bool _isEmpty;

    public void Init()
    {
        _accumulatedTS = new SqlTimeSpan();
        _isEmpty = true;
    }

    public void Accumulate(SqlTimeSpan tsToAdd)
    {
        if (!tsToAdd.IsNull)
            _accumulatedTS = SqlTimeSpan.Add(_accumulatedTS, tsToAdd);
        if (_isEmpty == true)
            _isEmpty = false;
    }

    public void Merge(SumTS group)
    {
        _accumulatedTS = SqlTimeSpan.Add(_accumulatedTS, group.Terminate());
    }

    public SqlTimeSpan Terminate()
    {
        SqlTimeSpan returnValue = SqlTimeSpan.Null;
        if (!_isEmpty)
            returnValue = _accumulatedTS;
        return returnValue;
    }

    #region IBinarySerialize Members

    void IBinarySerialize.Read(System.IO.BinaryReader r)
    {
        _isEmpty = r.ReadBoolean();
        if (!_isEmpty)
            _accumulatedTS.Read(r);
    }

    void IBinarySerialize.Write(System.IO.BinaryWriter w)
    {
        w.Write(_isEmpty);
        if (!_isEmpty)
            _accumulatedTS.Write(w);
    }

    #endregion
}

请注意,我们在序列化期间只调用 SqlTimeSpan 累加器的 ReadWrite。这确保了我们正确排序结果,因为所有必要的逻辑都已在 UDT 内部处理。

我们的聚合函数具有用户定义的序列化,因为我们也在使用用户定义的工件。我们将大小增加到 10,以便容纳一个指示聚合函数遍历的表是否为空的标志。如果表为空,我们返回 SQL-null;否则,我们返回累积值。这实际上是针对不起作用的 SqlUserDefinedAggregate.IsNullIfEmpty 属性的一种变通方法。根据 Microsoft 的文档,如果此属性设置为 true,则在将聚合函数应用于空表时,您应该得到 SQL-null,但似乎不起作用

这是我们的聚合函数在实际操作中的应用。

Aggregate

用户定义函数

我们还可以改进一些涉及日期的 SQL 函数。有了我们的 UDT,就不需要指定日期中的哪个部分参与计算,您也避免了繁琐的结果格式化任务。以下是这些 UDF 的代码。

static public partial class SqlTimeSpanUdfs
{    
    [SqlFunction]
    public static DateTime AddTS(DateTime dt, SqlTimeSpan ts)
    {
        return (dt.Add(TimeSpan.FromTicks(ts.Ticks)));
    }

    [SqlFunction]
    public static DateTime SubtractTS(DateTime dt, SqlTimeSpan ts)
    {
        return (dt.Subtract(TimeSpan.FromTicks(ts.Ticks)));
    }

    [SqlFunction]
    public static SqlTimeSpan DateDiff2(DateTime start, DateTime end)
    {
        TimeSpan ts = end - start;
        return new SqlTimeSpan(ts, false);
    }
    
    [SqlFunction]
    public static DateTimeOffset AddTSOffset(DateTimeOffset dt, SqlTimeSpan ts)
    {
        return (dt.Add(TimeSpan.FromTicks(ts.Ticks)));
    }

    [SqlFunction]
    public static DateTimeOffset SubtractTSOffset(DateTimeOffset dt, SqlTimeSpan ts)
    {
        return (dt.Subtract(TimeSpan.FromTicks(ts.Ticks)));
    }

    [SqlFunction]
    public static SqlTimeSpan DateDiffOffset(DateTimeOffset start, 
                              DateTimeOffset end)
    {
        TimeSpan ts = end - start;
        return new SqlTimeSpan(ts, false);
    }
};

以下是一些 UDF 在 DateTimeDateTimeOffset 上运行的示例。

Step 3

扩展方法

我们利用扩展方法提供了一种方便的方式,可以从系统时间间隔转换为其 UDT 等效项。我们为 UDT 提供的宽松相等运算符也可以应用于系统时间间隔,因此也作为扩展方法包含在内。

static public class TimeSpanExtensions
{
    static public SqlTimeSpan ToSqlTimeSpan(this TimeSpan ts)
    {
        return new SqlTimeSpan(ts, false);
    }

    static public bool ApproxEquals(this TimeSpan ts
                                  , TimeSpan tsToCompare
                                  , TimeSpan allowedMargin)
    {
        TimeSpan diff = ts - tsToCompare;
        return (diff.Duration() <= allowedMargin.Duration());
    }
}

有了这些,您应该能够在前端应用程序中编写如下代码。

// ToSqlTimeSpan
DateTime d1 = DateTime.Today;
DateTime d2 = DateTime.Now;
TimeSpan ts = d2-d1;
SqlParameter p = new
SqlParameter("@sqlTimeSpan", SqlDbType.Udt);
p.UdtTypeName = "SqlTimeSpan";
p.Value = ts.ToSqlTimeSpan();
cmd.Parameters.Add(p); // Where cmd is a valid  SqlCommand
cmd.ExecuteNonQuery();

// Comparing
TimeSpan AllowedMargin = TimeSpan.FromTicks(500);
TimeSpan ts1;
TimeSpan ts2;
// ts1 and ts2 get values here…
if (ts1.ApproxEquals(ts2,AllowedMargin))
{
    // Do something here
}

那么,这有什么意义呢?

我们的 SqlTimeSpan 终于准备就绪了,但问题是:它会好玩吗?这取决于。如果您是想拓宽技术视野并体验未来发展趋势的人,那就继续编码吧,但要为意外情况做好准备。UDT 需要相当大的学习曲线,并且您在优化路径上走得越远,爬坡就越陡峭。RAD 工具的支持仍然几乎不存在。作为一个基础工件,它的更改会产生非常广泛的影响,导致一些部署上的麻烦。至于好处,文字是很廉价的;您需要自己去发现它们。

历史

  • 2009年7月23日 - 首次发布。
  • 2009年7月27日 - 在 UDF 中添加了 DateTimeOffset 支持。
© . All rights reserved.