【问题标题】:Are there any pitfalls to using an IEnumerable<T> return type for SQL data?对 SQL 数据使用 IEnumerable<T> 返回类型有什么陷阱吗?
【发布时间】:2009-09-09 14:39:30
【问题描述】:

我的问题是基于以下代码的SQL连接状态、负载等:

public IEnumberable<MyType> GetMyTypeObjects()
{
  string cmdTxt = "select * from MyObjectTable";

  using(SqlConnection conn = new SqlConnection(connString))
  {
    using(SqlCommand cmd = new SqlCommand(cmdTxt, conn))
    {
      conn.Open();
      using(SqlDataReader reader = cmd.ExecuteReader())
      {
         while(reader.Read())
         {
            yield return Mapper.MapTo<MyType>(reader);
         }
       }
    }
  }
  yield break;
}

如果有许多进程运行类似代码并且在 IEnumerable 对象的迭代之间执行时间很长,我可以看到这可能是一个问题,因为连接将打开更长的时间等。但是,这似乎也有可能减少SQL 服务器上的 CPU 使用率,因为它仅在使用 IEnumerable 对象时返回数据。它还降低了客户端的内存使用量,因为客户端只需要在工作时加载一个 MyType 实例,而不是加载所有出现的 MyType(通过遍历整个 DataReader 并返回一个 List 或其他东西)。

  • 是否有任何您不希望以这种方式使用 IEnumerable 的实例,或者您认为它非常适合的任何实例?

  • 这会给 SQL 服务器带来什么样的负载?

  • 这是您会在自己的代码中使用的东西吗(除非提及 NHibernate、Subsonic 等)?

  • -

【问题讨论】:

    标签: c# sql-server ienumerable


    【解决方案1】:

    这不是我会遵循的模式。我不会像担心锁那样担心服务器上的负载。遵循这种模式将数据检索过程集成到您的业务逻辑流中,这似乎是一个全方位的解决方案;您不知道迭代端会发生什么,而您正在将自己插入其中。一次性检索您的数据,然后在您关闭阅读器后允许客户端代码对其进行枚举。

    【讨论】:

    • 锁定 SQL 对象?我没有考虑到这一点。您如何看待提供 nolock 提示(仅在某些情况下可行)?
    • 这将(显然)消除锁定问题,但您仍然在未定义的时间段内保持阅读器打开(如果消费代码忘记处理枚举器,可能永远保持打开状态)。低收益似乎是一个相对较高的风险。如果对这种做法有特别的需求——我认为内存消耗是合格的——那么它并不是完全邪恶的,但我绝对不会让它成为一种习惯,我会寻找替代方法。
    • 顺便说一句,您决不能保证“MyType 的一个实例”。事实上,即使消费代码一次只处理一个实例,GC 也不会立即清理它们。是的,您将让它们超出范围并有资格尽快收集,但这不会是持续的内存占用。
    • @scottm - nolock 提示经常被误用。如果您打算使用它 - 您应该确保您完全了解它对您的应用程序的所有影响。
    • @Scott Ivey,我理解其中的含义。这就是为什么我说它只在某些情况下可行。
    【解决方案2】:

    我不会使用它,因为它隐藏了正在发生的事情,并且可能会在没有适当处理的情况下留下数据库连接。

    当您读完最后一条记录时,连接对象将被关闭,如果您在此之前停止读取,则不会释放连接对象。例如,如果您知道结果中始终有 10 条记录,并且只有一个循环从枚举器中读取这 10 条记录,而没有进行超出最后一项的第十一次读取调用,则连接没有正确关闭。此外,如果您只想使用部分结果,则无法在不读取其余记录的情况下关闭连接。

    即使是枚举器的内置扩展,即使您正确地使用它们,也可能导致这种情况:

    foreach (MyType item in GetMyTypeObjects().Take(10)) {
       ...
    }
    

    【讨论】:

    • 这是一个好点,也没有考虑过。我被它的使用方式蒙蔽了双眼!
    • @Guffa:使用诸如Take 之类的扩展方法不会有问题:GetMyTypeObjects 方法只是创建IDisposable 迭代器对象的语法糖。 Take 将在完成后调用迭代器的 Dispose 方法,然后处理连接、命令、读取器等。
    • @Guffa:这同样适用于结果集的任何其他部分读取:只要您在完成后调用 Dispose(或者最好将所有内容包装在 using 块中)那么连接、命令、阅读器等也会被处理掉。
    • @Luke:是的,你是对的。我对此进行了测试,不知何故,它设法创建了一个似乎做正确事情的 Dispose 方法,但我不确定它如何知道要处理哪些对象......
    • Eric Lippert 有一些关于迭代器内部工作原理的精彩帖子:blogs.msdn.com/ericlippert/archive/tags/Iterators/default.aspx
    【解决方案3】:

    我建议不要进行预优化。在许多情况下,连接将被池化。

    我也不希望 SQL Server 上的负载有任何差异 - 查询已经编译,并且将运行。

    【讨论】:

    • 我不确定您所说的预优化是什么意思(我没有尝试这样做)。我认为负载会受到影响,因为服务器会同时在更多连接上保持光标位置。
    • 通过预优化,我的意思是您在代码工作之前担心性能问题。您正在考虑可能不存在的问题。
    • 我明白了。我也不想让自己陷入困境,不得不重新设计我的数据访问方法以及将来使用它们的方法。
    【解决方案4】:

    我担心的是您完全受制于客户端代码。

    您已经提到调用代码可能会保持连接打开的时间超过严格必要的时间,但也有可能永远不允许关闭/处置对象。

    只要客户端代码使用foreachusing 等或显式调用枚举器的Dispose 方法就可以了,但没有什么可以阻止它执行以下操作:

    var e = GetMyTypeObjects().GetEnumerator();
    e.MoveNext();    // open the connection etc
    
    // forget about the enumerator and go away and do something else
    // now the reader, command and connection won't be closed/disposed
    // until the GC kicks in and calls their finalisers
    

    【讨论】:

      猜你喜欢
      • 2019-02-20
      • 2010-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-28
      相关资源
      最近更新 更多