【问题标题】:Supplying stream as a source of data for a binary column when SqlBulkCopy is used使用 SqlBulkCopy 时提供流作为二进制列的数据源
【发布时间】:2016-05-11 07:49:03
【问题描述】:

如果需要以流的方式 SqlServer 读取数据,那么有一些功能可以做到这一点。比如使用SqlDataReaderCommandBehavior.SequentialAccess,特别是当需要访问二进制列数据时,有GetStream(int)方法:

var cmd = new SqlCommand();
cmd.Connection = connection;
cmd.CommandText = @"select 0x0123456789 as Data";

using (var dr = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
{
    dr.Read();

    var stream = dr.GetStream(0);
    // access stream
}

但是如果需要使用SqlBulkCopy 向 SqlServer 提供数据时,特别是如果需要将流作为二进制列的数据源提供时,反方向的流数据呢? ?

我试过了

var cmd2 = new SqlCommand();
cmd2.Connection = connection;
cmd2.CommandText = @"create table #Test (ID int, Data varbinary(max))";
cmd2.ExecuteNonQuery();

using (SqlBulkCopy sbc = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, null))
{
    sbc.DestinationTableName = "#Test";
    sbc.EnableStreaming = true;

    sbc.ColumnMappings.Add(0, "ID");
    sbc.ColumnMappings.Add(1, "Data");

    sbc.WriteToServer(new TestDataReader());
}

其中TestDataReader 实现IDataReader 如下:

class TestDataReader : IDataReader
{
    public int FieldCount { get { return 2; } }
    int rowCount = 1;
    public bool Read() { return (rowCount++) < 3; }
    public bool IsDBNull(int i) { return false; }

    public object GetValue(int i)
    {
        switch (i)
        {
            case 0: return rowCount;
            case 1: return new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89 };
            default: throw new Exception();
        }
    }

    //the rest members of IDataReader
}

它按预期工作。

但是变化

case 1: return new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89 };

case 1: return new MemoryStream(new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89 });

导致异常System.InvalidOperationException与消息

来自数据源的 MemoryStream 类型的给定值不能 转换为指定目标列的 varbinary 类型。

有没有办法将StreamIDataReader(或者可能是DbDataReader)提供到SqlBulkCopy 作为二进制列的数据源,而无需先将其所有数据复制到内存(字节数组)中?

【问题讨论】:

  • 您对如何使用自定义 IDataReader 来实现这一点感兴趣,或者实际上您将使用一些现有的数据读取器(如 SqlDataReader)?
  • @Evk,带有自定义 IDataReaderDbDataReader(我正在使用从二进制/xml 文件提供数据的自定义实现,但是像我的问题中的一些虚拟实现就足够了)。跨度>
  • 我试图检查SqlBulkCopyTdsParser 的内部结构(在ReferenceSource 上)。看来这应该是可能的。 SqlBulkCopy 调用DbDataReader.GetStream(int) 方法,然后将结果包装到StreamDataFeed,然后传递给TdsParser.WriteStreamFeed() 方法。我试图从DbDataReader 继承我的数据阅读器,但我无法调用GetStream(int)。似乎我错过了什么。
  • 当你写你的评论时,我正在写一个与这个提议完全相同的答案:) 但是我确实测试了它并且它对我来说很好(使用你的示例中的代码)。

标签: c# sql-server ado.net sqlbulkcopy idatareader


【解决方案1】:

不确定这是否记录在任何地方,但如果对SqlBulkCopy 源代码进行简短检查,您可能会发现它以不同的方式处理不同的数据读取器。 首先,SqlBulkCopy 确实支持流式传输和GetStream,但您可能会注意到IDataReader 接口不包含GetStream 方法。因此,当您将自定义 IDataReader 实现提供给 SqlBulkCopy 时 - 它不会将二进制列视为流式传输,并且不会接受 Stream 类型的值。

另一方面 - DbDataReader 确实有这个方法。如果您为 SqlBulkCopy 提供 DbDataReader-inherited 类的实例 - 它将以流式方式处理所有二进制列并调用 DbDataReader.GetStream

所以要解决您的问题 - 像这样从 DbDataReader 继承:

class TestDataReader : DbDataReader
{
    public override bool IsDBNull(int ordinal) {
        return false;
    }

    public override int FieldCount { get; } = 2;
    int rowCount = 1;

    public override bool HasRows { get; } = true;
    public override bool IsClosed { get; } = false;

    public override bool Read()
    {
        return (rowCount++) < 3;
    }

    public override object GetValue(int ordinal) {
        switch (ordinal) {
            // do not return anything for binary column here - it will not be called
            case 0:
                return rowCount;
            default:
                throw new Exception();
        }
    }

    public override Stream GetStream(int ordinal) {
        // instead - return your stream here
        if (ordinal == 1)
            return new MemoryStream(new byte[] {0x01, 0x23, 0x45, 0x67, 0x89});
        throw new Exception();
    }
    // bunch of irrelevant stuff

}

【讨论】:

  • 好的,我找到了为什么 GetStream 在我第二次尝试阅读器实现时没有被调用(继承自 DbDataReader 而不是 IDataReader)。可能在使用不同方向的代码时,我引入了一个错误(我在某些时候将目标列更改为varbinary(100) 而不是varbinary(max))。您的代码与问题代码相结合运行良好。太好了,这是可能的!谢谢。
【解决方案2】:

见以下代码

static int SendOrders(int totalToSend)
    {
      using (SqlConnection con = new SqlConnection(connectionString))
      {
        con.Open();
        using (SqlTransaction tran = con.BeginTransaction())
        {
          var newOrders =
                  from i in Enumerable.Range(0, totalToSend)
                  select new Order
                  {
                    customer_name = "Customer " + i % 100,
                    quantity = i % 9,
                    order_id = i,
                    order_entry_date = DateTime.Now
                  };

          SqlBulkCopy bc = new SqlBulkCopy(con,
            SqlBulkCopyOptions.CheckConstraints |
            SqlBulkCopyOptions.FireTriggers |
            SqlBulkCopyOptions.KeepNulls, tran);

          bc.BatchSize = 1000;
          bc.DestinationTableName = "order_queue";
          bc.WriteToServer(newOrders.AsDataReader()); 

          tran.Commit();
        }
        con.Close();

      }

      return totalToSend;

    }

【讨论】:

  • 对不起,但这似乎没有帮助。我的问题中已经有SqlBulkCopy 用法示例。问题是关于这种用法的细节。特别是,是否可以通过IDataReader + SqlBulkCopyStream 数据馈送到二进制列,而无需先将所有流数据复制到内存中。
猜你喜欢
  • 2015-09-23
  • 2011-04-28
  • 2015-09-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多