SqlClient 流式处理支持

使用 Microsoft.Data.SqlClient 流式处理方法来处理大值,无需先将完整值加载到应用内存中。 流式传输对于二进制文件、大文本、XML、JSON 以及服务器间传输非常有用。

流式处理不会使该值可查找,也不会使读取器上的并发操作变得安全。 连接、命令、读取器、源流和目的对象必须保持存活,直到传输完成。

本文中的示例假定一个名为 connection 的已打开 SqlConnection 指向 AdventureWorks 示例数据库。

C# 示例要求 using Microsoft.Data.SqlClient;。 使用 SqlDbType 或 CommandBehavior 的示例也需要 using System.Data;。

从 SQL 数据库中流式传输数据

使用 CommandBehavior.SequentialAccess 执行该命令,并按序号顺序读取列。 使用顺序访问时,当你越过一列后,不要再读同一行的前一列。

请使用以下 SqlDataReader 方法:

方法 结果 典型的SQL数据
GetStream Stream 二进制、varbinary、图像和用户定义类型 (UDT) 数据
GetTextReader TextReader 字符和 JSON 数据
GetXmlReader XmlReader xml

在请求可为 null 的列的流之前调用 IsDBNull。 一个流获取器与不兼容的 SQL 类型一起使用时,会抛出异常。 在读取下一列、推进行或进入下一个结果之前,先消耗或丢弃当前流。

GetStream 不受加密列支持。 GetTextReader 不支持对加密列的顺序访问。

CommandTimeout 会在 Microsoft.Data.SqlClient 执行和读取期间等待网络数据包时生效。 你的应用程序处理已接收数据所花费的时间不计入。 关于取消和超时,请参见 异步编程。

流式传输二进制数据

AdventureWorks 将文件内容存储在可为空的 varbinary(max)Production.Document.Document 列中。 本示例使用有界缓冲区将一个值复制到文件中:

using var command = new SqlCommand("""
    SELECT TOP (1) Document
    FROM Production.Document
    WHERE Document IS NOT NULL
    ORDER BY ModifiedDate DESC;
    """, connection);

using SqlDataReader reader = command.ExecuteReader(
    CommandBehavior.SequentialAccess);
if (reader.Read())
{
    using Stream source = reader.GetStream(0);
    using FileStream destination = File.Create("document.bin");
    source.CopyTo(destination);
}

对于文本,使用 GetTextReader 并复制到 TextWriter。 对于 XML,使用 GetXmlReader 处理 XML 节点,但不将完整值转换为字符串。

将数据流式传输到 SQL 数据库

将 Value 设置为流式对象,并选择兼容的 SQL 类型:

值类型 兼容的参数类型
Stream Binary, VarBinary, 以及遗留 Image 类型
TextReader Char、NChar、Text、NText、旧版 Xml 和 NVarChar 类型、VarChar,以及 Json
XmlReader Char、NChar、Text、NText、旧版的 Xml 和 NVarChar 类型、VarChar 和 Json

显式设置 Size 。 Size 限制传输量。 当你想发送完整源时,对 (max) 参数使用 -1。

对于有界内存传输,可以使用 VarBinary、VarChar 或 NVarChar 配合 Size = -1,或者使用 Xml 或 Json。 对于非 (max) 字符类型和二进制类型,以及旧版 Image、Text 和 NText 类型,Microsoft.Data.SqlClient 会在执行前将源缓冲到字节数组或字符串中。

流式传输参数值

该示例通过 varbinary(max) 参数发送文件,返回接收到的字节数。 它不存储该值:

using FileStream input = File.OpenRead("document.bin");
using var command = new SqlCommand(
    "SELECT DATALENGTH(@content);",
    connection);
command.Parameters.Add(
    new SqlParameter("@content", SqlDbType.VarBinary, -1)
    {
        Value = input
    });

long length = (long)command.ExecuteScalar()!;
Console.WriteLine($"{length} bytes");

在命令执行完成前保持输入对象开启。 将取消令牌传递给异步执行方法,或调用 Cancel 请求流操作的取消。 取消操作会尽力执行,但不保证成功。 操作可能会在取消生效前完成。

来自源流或读取器的异常会导致命令执行失败。 除非你能确定该对象的位置且数据库操作可以安全重复,否则不要从同一个对象重试。

将数据从一个 SQL 数据库流式传输到另一个 SQL 数据库

此示例将一个 AdventureWorks 文档流直接传递给另一个名为 destinationConnection 的已打开 SqlConnection 上的参数。 示例命令报告接收到的长度。 用目标数据库的参数化插入或更新命令替代:

using var readSource = new SqlCommand("""
    SELECT TOP (1) Document
    FROM Production.Document
    WHERE Document IS NOT NULL
    ORDER BY ModifiedDate DESC;
    """, connection);

using SqlDataReader sourceReader = readSource.ExecuteReader(
    CommandBehavior.SequentialAccess);

if (sourceReader.Read())
{
    using Stream content = sourceReader.GetStream(0);
    using var write = new SqlCommand(
        "SELECT DATALENGTH(@content);",
        destinationConnection);
    write.Parameters.Add(
        new SqlParameter("@content", SqlDbType.VarBinary, -1)
        {
            Value = content
        });

    Console.WriteLine(write.ExecuteScalar());
}

源命令和目标命令不共享事务。 如果你的应用需要跨服务器协调原子工作,则要单独设计和测试该事务边界。