【问题标题】:Is it safe to reuse a SqlDataRecord?重用 SqlDataRecord 是否安全?
【发布时间】:2016-05-03 14:30:52
【问题描述】:

在实现表值参数时,生成IEnumerable<SqlDataRecord> 以供参数使用的最常见方法之一是这样的代码(例如,https://stackoverflow.com/a/10779567/18192):

public static IEnumerable<SqlDataRecord> Rows(List<int> simpletable)
{
    var smd = new []{ new SqlMetaData("id", SqlDbType.Int)};
    var sqlRow = new SqlDataRecord(smd);
    foreach (int i in simpletable)
    {
        sqlRow.SetInt32(0, i);
        yield return sqlRow;
    }
}
//...
var param = sqlCmd.Parameters.AddWithValue("@retailerIDs", Rows(mydata)); 
param.SqlDbType = SqlDbType.Structured;
param.TypeName = "myTypeName";

此代码似乎确实有效。虽然重用SqlMetaData 不会引起太多警钟,但在foreach 循环之外声明SqlDataRecord 对我来说非常可疑:

一个可变对象被修改然后重复生成。

作为一个令人担忧的例子,在 LinqPad 中调用 var x = Rows(new[] { 100, 200}.ToList()).ToList().Dump() 会输出 200,200。这种方法似乎依赖于实现细节(行被单独处理),但我没有看到任何承诺这一点的文档。

是否有一些缓解因素使这种方法安全?

【问题讨论】:

  • 乍一看确实很可疑。 SqlDataRecord 继承自 System.Object,所以它是一个引用类型。如果这是一个普通的 foreach 循环,您将重复返回同一个对象。但是我认为 yield 关键字改变了一些事情。为每个项目调用一次方法 Rows,因此为每一行实例化一个新的 SqlDataRecord。请参阅 MSDN > yield(C# 参考):msdn.microsoft.com/en-us/library/9k7k7cf0.aspx
  • @RichardCL:SqlDataRecord 是一个引用类型。因此,yield return 正在吐出引用的副本,但该引用每次都指向同一个对象。因此,为什么我的示例 sn-p (var x = Rows(new[] { 100, 200}.ToList()).ToList().Dump()) 会吐出200,200 而不是100,200。代码在 SqlCommand 的情况下有效的唯一原因是每一行都是单独处理的......但我相信这是一个实现细节,而不是记录的内容。
  • 没有办法让这段代码安全,除非它在一个线程上运行,并且返回值从不在它的枚举器之外使用- 从不转换为列表、数组或其他任何东西。这将使它无用,例如批处理操作,在网格上显示等。
  • @PanagiotisKanavos:正如我所说,这段代码被用作表值 SP 参数,所以这些问题都不适用,除非它们被各种 @ 的实现细节所违反987654336@ 来电。
  • @Brian 你应该用相反的方式表达这个:这段代码可以工作的唯一方法是如果Execute非常 特定的方式编码并且没有其他代码结果在迭代器的枚举中,例如没有ToListToArray 调用。否则,您会将一组 X 引用传递给同一对象。这么说很明显这段代码是不安全的。

标签: c# sql-server table-valued-parameters


【解决方案1】:

这种方法似乎依赖于实现细节(行是 单独处理),但我没有看到任何文件 承诺这一点。

是否有一些缓解因素可以使这种方法安全?

正如 user1249190 指出的那样,https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.server.sqldatarecord#remarks 的备注部分明确建议重用 SQLDataRecord:

该类与 SqlPipe 一起使用,将结果集发送到 来自托管代码存储过程的客户端。写常见的时候 语言运行时 (CLR) 应用程序,您应该重用现有的 SqlDataRecord 对象,而不是每次都创建新对象。 创建许多新的 SqlDataRecord 对象可能会严重耗尽内存 并对性能产生不利影响。

显然,此建议不适用于跨线程使用:文档还明确警告“任何实例成员都不能保证是线程安全的。”

【讨论】:

  • 该注释特定于在 CLR 存储过程中使用 SqlPipe。似乎 SqlDataRecord 文档是在这是主要/唯一用例时编写的。现在它们也被用于另一端的 tvp 参数,同样的逻辑可能适用......或不适用。正如 Brian 在 cmets 下面的几个答案中所说,问题实际上是消费者计划如何使用 IEnumerable - 急切还是懒惰?在发送 tvp 方面,所引用的示例完全有效的事实表明 tvp 评估是惰性的
【解决方案2】:

如果您在 foreach 循环之外根本不需要它,我不明白您为什么要重复使用它。

我发现这个问题Is there a reason for C#'s reuse of the variable in a foreach? 链接到另一个问题Is it better coding practice to define variables outside a foreach even though more verbose? 中的这个答案,Jon Skeet 回答说:

在循环之外声明变量没有任何好处,除非你想在迭代之间保持它们的值。

(请注意,这通常不会产生行为差异,但如果变量是由 lambda 表达式或匿名方法捕获的,则情况并非如此。)

【讨论】:

  • 重用它可以节省分配——在这种情况下我们关心的是,如果我们不这样做,我们将使用DataTable。至于在这种情况下节省多少是另一回事,因为这些对象都是短暂的,应该很容易被 GC 提供。
  • @JeroenMostert 在循环内修改同一个共享实例只会确保调用者得到错误的数据。如果性能是一个问题,应该使用 SqlBulkCopy、not TVP 和 SqlDataRecord。由于使用批处理操作、最少的日志记录等,速度存在巨大差异。无论如何,尝试保存分配是没有意义的——这只有在 ADO.NET 本身复制数据时才有效在迭代期间,没有其他代码尝试枚举迭代器。
  • @PanagiotisKanavos:实际上,调用者最终会获得正确的数据,因为调用者会单独处理行。当然,依赖它是肮脏的。至于性能优势,如果处理较小的行集,TVP 比批量插入更快。请参阅MSDN: Use Table-Valued Parameters 上的表值参数与 BULK INSERT 操作
  • @Brian “在实践中”实际上意味着“在非常具体的情况下”。您自己的示例显示了这如何失败。至于性能优势 - 如果行数很少,您为什么关心重用 SqlDataRecord?
  • @PanagiotisKanavos:我不确定称其为“非常具体”是否公平。我认为应用程序代码很少为表值参数以外的任何东西实例化 SqlDataRecord。通常,使用IENumerable&lt;SqlDataRecord&gt; 的编写良好的库不太可能使用急切求值,因为这样做不仅会强制对 SqlDataRecords 进行额外的迭代,而且还因为惰性求值会持续使用内存。依赖实现细节是不好的,但它不太可能真正中断(如果确实如此,它可能会立即中断)。
【解决方案3】:

不,重复使用变量是不安全的。代码一遍又一遍地修改同一个对象。这是错误的代码,应该返回一个新对象。这是一个快速的 linqpad 示例,显示了上述代码的问题:

void Main()
{
    //This code proves that the object is being modified.
   Thing prevRow = null;
    foreach (var curRow in Rows(new List<int>() { 1, 2, 3 }))
    {
        Console.WriteLine(curRow);
        Console.WriteLine(prevRow);
        prevRow = curRow;
    }

    //Because the object is modified instead of a new object being returned,
    // this code does something unexpected; it returns the same object 3
    // times! Instead of three unique objects representing the values 1, 2, 3.
    var rowsAsList = Rows(new List<int>() { 1, 2, 3 }).ToList();
    foreach (var curRow in rowsAsList)
    {
        Console.WriteLine(curRow);
    }
}

public class Thing
{
    public int i;
}

IEnumerable<Thing> Rows(List<int> simpletable)
{
    var sqlRow = new Thing() {i=-1};
    foreach (int i in simpletable)
    {
        sqlRow.i = i;
        yield return sqlRow;
    }
}

【讨论】:

  • 这个演示与我的示例大体相同,“作为一个令人担忧的示例,在 LinqPad 中调用 var x = Rows(new[] { 100, 200}.ToList()).ToList().Dump() 会吐出 200,200。”
  • 这正是我试图向您展示的,这就是为什么代码应该返回一个新事物而不是一遍又一遍地重复使用相同的“事物”:)
  • 但是MSDN说要重用备注里的对象?? msdn.microsoft.com/en-us/library/…
猜你喜欢
  • 2011-01-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-22
相关资源
最近更新 更多