什么是 IBinarySerialize 接口方法?

What are IBinarySerialize Interface methods used for?

创建自定义聚合函数时需要指定 enumeration format:

Format Enumeration is used by SqlUserDefinedTypeAttribute and SqlUserDefinedAggregateAttribute to indicate the serialization format of a user-defined type (UDT) or aggregate.

并且当使用 UserDefined 格式时,您的 class 需要实现 IBinarySerialize Interface 并覆盖其 readwrite 方法。

我的问题是这些方法到底需要做什么?

examples,我猜他们应该可以read/write聚合结果?

例如,我正在尝试创建一个 SQL 连接不同数字的 CLR 函数。在 T-SQL 中,我可以有 1 到 255 个不同的数字(TINYINT 值)。我需要从它们创建一个字符串(使用定界符),但也要对数字进行排序。该功能似乎有效,但我不确定是否已按预期重写这些方法:

[Serializable]
[
    Microsoft.SqlServer.Server.SqlUserDefinedAggregate
    (
        Microsoft.SqlServer.Server.Format.UserDefined,
        IsInvariantToNulls = true,
        IsInvariantToDuplicates = true,
        IsInvariantToOrder = false,
        MaxByteSize = 1024
    )
]
public class ConcatenateAnswersPos : Microsoft.SqlServer.Server.IBinarySerialize
{
    private List<byte> intermediateResult;

    public void Init()
    {
        intermediateResult = new List<byte>();
    }

    public void Accumulate(SqlByte value)
    {
        intermediateResult.Add((byte)value);
    }

    public void Merge(ConcatenateAnswersPos other)
    {
        intermediateResult.AddRange(other.intermediateResult);
    }

    public SqlString Terminate()
    {
        if (intermediateResult != null)
        {
            intermediateResult.Sort();
            return new SqlString(string.Join(";", intermediateResult));
        }
        else
        {
            return new SqlString("");
        }

    }

    public void Read(BinaryReader r)
    {
        if (r == null) throw new ArgumentNullException("r");

        intermediateResult = new List<byte>();
        string[] answers = r.ReadString().Split(';');

        foreach (string answer in answers)
        {
            intermediateResult.Add(Convert.ToByte(answer));
        }
    }

    public void Write(BinaryWriter w)
    {
        if (w == null) throw new ArgumentNullException("w");
        intermediateResult.Sort();
        w.Write(string.Join(";", intermediateResult));
    }
}

您的聚合可以在处理其操作的行的一半时被序列化和删除。然后数据库引擎可以创建一个新实例并反序列化以返回到它停止的地方。

因此,Write 方法需要能够在仅将部分记录传递给 Accumulate 时存储聚合状态。 Read 方法需要能够为 AccumulateMerge.

上的更多调用准备好聚合备份

因此我会说你已经正确地实现了这些。

我会说你在你的方法上做的工作比你需要的多。您需要做的就是在 Write 方法中编写足够的内容,以便您的 Read 方法可以重建您的内部状态。由于您的内部状态只是 List<byte>,因此无需将所有内容都视为字符串:

public void Read(BinaryReader r)
{
    if (r == null) throw new ArgumentNullException("r");

    var count= r.ReadInt32();

    intermediateResult = new List<byte>(count);
    for (int i=0;i<count;i++)
    {
        intermediateResult.Add(r.ReadByte());
    }
}

public void Write(BinaryWriter w)
{
    if (w == null) throw new ArgumentNullException("w");
    w.Write(intermediateResult.Count);
    foreach(byte b in intermediateResult)
    {
      w.Write(b);
    }
}

正如我在评论中建议的那样,我还从 Write 方法中删除了 Sort,因为 [=17] 中总会有一个最终的 Sort 调用=] 在您构建的数据传递给您的聚合消费者之前。


我们先把Count存入数据,这样我们就知道在Read方法中调用了多少次ReadByte。这也允许进行(可能毫无意义的)优化,我们可以告诉 List<byte> 构造函数它需要 space 的确切项目数量。

IBianarySerialize 方法用于保存您的对象并在需要将其写入磁盘时恢复它。

因此,Write method should save everything needed to recreate the object in its current state (the data) and the Read 方法应该采用 Write 的输出并设置对象的状态(因此它与原始状态匹配)。

其他答案似乎很好地解决了这个过程中的问题,但我确实建议使用这些方法尽可能小和快速地保持你的数据reading/writing。

不保证用户定义聚合 (UDA) 的任何特定实例在查询的整个生命周期中都存在。它需要有一个可存储的表示。当您仅使用值类型时(如问题中 "enumeration format" link 中所述),提供的 ReadWrite 方法了解如何序列化和反序列化 UDA,在在哪种情况下您会使用 Format.Native。但是当你开始使用引用类型(字符串、集合、自定义类型等)时,你需要定义这些值如何序列化和反序列化,在这种情况下你需要使用 Format.UserDefined 并覆盖 ReadWrite 方法,以便您可以控制这些操作。

需要序列化的值是将 UDA 的新实例恢复到序列化之前的 exact 状态所需的任何值。这意味着:不要依赖 Init() 方法 运行ning(它 运行 每组一次!)或变量初始化器(它们 运行 每个实例化一次,并且 UDA 可以是无需重新创建即可重新用于多个组!)。所以你需要序列化所有的基值,即使它们与最终输出没有直接关系。


就是说,您至少应该进行@Damien_The_Unbeliever 的回答中提到的优化:

  • 不要在 Write 方法中进行排序。你已经在Terminate方法(合适的地方)做了,做两次也没用,更何况效率很低。

  • 存储集合的计数,然后存储单个元素

除此之外:

  • 当你说你的 UDA "concatenates distinct numbers" 如果你的意思是 "distinct" 那么你需要检查每个进来的数字,看看它是否已经在列表中.我怀疑这是您的愿望,因为您已将 IsInvariantToDuplicates 设置为 true。您可以在 Accumulate 方法中执行此操作:

    if (!intermediateResult.Contains(value.Value))
    {
      intermediateResult.Add(value.Value);
    }
    

    并且在Merge方法中(涉及并行时调用):

    foreach (byte _NewValue in other.intermediateResult)
    {
      if (!intermediateResult.Contains(_NewValue))
      {
        intermediateResult.Add(_NewValue);
      }
    }    
    

    请注意,我已将 Accumulate 方法中的 (byte)value 转换为使用 Value 属性。所有 SqlTypes(例如 SqlByteSqlStringSqlInt32 等)都有一个 Value 属性 return您期望的 .NET 类型。这意味着不需要像许多人那样在 SqlString 上调用 ToString()

  • 我会对 1024 的 MaxByteSize 持谨慎态度。考虑到在字符串中保存“165;207”(当前方法),实施@Damien 的建议可以部分缓解这种担忧技术上是 14 个字节(每个字符 7 个字符 * 2 个字节),而保存计数和单个字节仅为 6 个字节(Int32 的 4 个字节用于存储 count + 2 个单独的字节)。而这种差异只是为了存储 2 个值。存储200?是啊!

  • 您没有指定 IsNullIfEmpty 属性。您需要指定这一点,特别是考虑到如果内部集合是 null,您的 Terminate 方法是 returning 一个空字符串。你应该添加 IsNullIfEmpty = false 因为你不想 return NULL 如果它从未被调用过。

  • 可能不需要 Terminate 方法中处理 null 集合的额外逻辑。该集合是在 InitRead 方法中初始化的,所以我不确定在调用 Terminate 时它怎么会是 null


如果您想要使用 Format.UserDefined 创建用户定义聚合的示例,请查看 Getting The Most Out of SQL Server 2005 UDTs and UDAs(需要免费注册)。我是在 SQL Server 2008 发布之前写的,它允许序列化超过 8000 个字节,因此您可以(暂时)忽略有关压缩数据以序列化的方面。

此外,如果您想了解有关 SQLCLR 的更多信息,我正在为 SQL Server Central 编写一系列关于它的文章:Stairway to SQLCLR(与第一个站点相同link编辑文章)。