【问题标题】:Return DataReader from DataLayer in Using statement在 Using 语句中从 DataLayer 返回 DataReader
【发布时间】:2010-10-25 09:42:19
【问题描述】:

我们有很多数据层代码遵循这个非常普遍的模式:

public DataTable GetSomeData(string filter)
{
    string sql = "SELECT * FROM [SomeTable] WHERE SomeColumn= @Filter";

    DataTable result = new DataTable();
    using (SqlConnection cn = new SqlConnection(GetConnectionString()))
    using (SqlCommand cmd = new SqlCommand(sql, cn))
    {
        cmd.Parameters.Add("@Filter", SqlDbType.NVarChar, 255).Value = filter;

        result.Load(cmd.ExecuteReader());
    }
    return result;
}

我认为我们可以做得更好。我现在的主要抱怨是它强制将所有记录加载到内存中,即使对于大型集合也是如此。我希望能够利用 DataReader 一次只在 ram 中保留一条记录的能力,但是如果我直接返回 DataReader,则在离开 using 块时连接将被切断。

如何改进这一点以允许一次返回一行?

【问题讨论】:

  • 但是将所有记录加载到内存中通常不是比使用 DataReader 保持与数据库的连接打开更好吗?
  • 视情况而定。对于 winforms 应用程序,是的。对于内存稀缺且无论如何查询都需要快速完成的 Web 应用程序,可能不需要。
  • 您能否更详细地说明您希望拥有什么样的“DataReader 的真正优势”?
  • 所以你想使用 DataReader 因为它更快地返回第一个结果?
  • 与其专注于使用数据读取器的“优雅”方式,我会花更多时间处理 sql 查询本身。为什么要返回这么多行以致内存消耗成为问题?查看您的实现让我觉得查询返回的许多行从未被使用过。

标签: c# .net database .net-2.0


【解决方案1】:

再一次,为这个问题构思我的想法的行为揭示了答案。具体来说,我写的最后一句“一次一行”。我意识到我并不真正关心它是一个数据读取器,只要我可以逐行枚举它。这导致我这样做:

public IEnumerable<IDataRecord> GetSomeData(string filter)
{
    string sql = "SELECT * FROM [SomeTable] WHERE SomeColumn= @Filter";

    using (SqlConnection cn = new SqlConnection(GetConnectionString()))
    using (SqlCommand cmd = new SqlCommand(sql, cn))
    {
        cmd.Parameters.Add("@Filter", SqlDbType.NVarChar, 255).Value = filter;
        cn.Open();

        using (IDataReader rdr = cmd.ExecuteReader())
        {
            while (rdr.Read())
            {
                yield return (IDataRecord)rdr;
            }
        }
    }
}

一旦我们迁移到 3.5 并且可以开始在结果上使用其他 linq 运算符,这将工作得更好,我喜欢它,因为它让我们开始思考每层之间的“管道”,用于返回的查询很多结果。

不利的一面是,对于持有多个结果集的读者来说会很尴尬,但这种情况非常罕见。

更新
自从我在 2009 年第一次开始使用这种模式以来,我了解到最好也将其设为通用的 IEnumerable&lt;T&gt; 返回类型并添加一个 Func&lt;IDataRecord, T&gt; 参数以将 DataReader 状态转换为循环中的业务对象。否则,延迟迭代可能会出现问题,以至于您每次都看到查询中的最后一个对象。

【讨论】:

  • 我正在为 .NET 3.5 使用类似的实现。它工作得很好,但由于某种原因,我觉得迭代器模式有一些误用,但这非常主观。当您想要包装一些异常处理以防止此块退出时,它真的会变得很混乱,因为由于某种原因强制转换失败。 ;-)
  • @RunningMonkey:既然你已经实时运行了这个模式,你是否经常看到 (IDataRecord) 转换失败?我应该有多担心?
  • 可能不多,因为我的实现更类似于stackoverflow.com/questions/47521/…。在进行收益返回之前,我使用 Convert.ChangeType 转换为基本类型。这几乎是在找麻烦。 :-)
  • 说实话:IDataRecord 的演员阵容对我来说更具吸引力。
  • 此解决方案将保持您的连接打开,直到您迭代整个结果集,而原始代码使用 firehose 光标填充 DataTable 并尽快关闭连接。实际上,您正在用一种资源(内存)换取另一种(数据库连接),不确定这是否对您来说是个问题。
【解决方案2】:

你想要的是一个受支持的模式,你必须使用

cmd.ExecuteReader(CommandBehavior.CloseConnection);

并从您的 GetSomeData() 方法中删除两个 using()。异常安全必须由调用者提供,以保证读者关闭。

【讨论】:

    【解决方案3】:

    在这种情况下,我发现 lambdas 非常有用。考虑到这一点,而不是数据层给我们数据,让我们给数据层我们的数据处理方法:

    public void GetSomeData(string filter, Action<IDataReader> processor)
    {
        ...
    
        using (IDataReader reader = cmd.ExecuteReader())
        {
            processor(reader);
        }
    }
    

    然后业务层会调用它:

    GetSomeData("my filter", (IDataReader reader) => 
        {
            while (reader.Read())
            {
                ...
            }
        });
    

    【讨论】:

    • .Net 2.0 现在是,但我会在下次更新时记住这一点。
    • 实际上,我认为这不会起作用,因为我希望将这些传递到表示层的链上。我仍然可以通过在 lambda 中重复这种模式来做到这一点,但这感觉很难看。
    • 我同意。再说一次,我倾向于尽快完成并关闭数据读取器,而是在数据层构建所需的数据,将其留给上面的层来决定这些数据的外观。使用 Action 处理器,业务层可以指定格式,同时保持阅读器循环尽可能紧凑(假设业务层不会在处理器方法中做一些耗时的事情)。
    【解决方案4】:

    关键是yield关键字。

    与 Joel 的原始答案类似,更加充实:

    public IEnumerable<S> Get<S>(string query, Action<IDbCommand> parameterizer, 
                                 Func<IDataRecord, S> selector)
    {
        using (var conn = new T()) //your connection object
        {
            using (var cmd = conn.CreateCommand())
            {
                if (parameterizer != null)
                    parameterizer(cmd);
                cmd.CommandText = query;
                cmd.Connection.ConnectionString = _connectionString;
                cmd.Connection.Open();
                using (var r = cmd.ExecuteReader())
                    while (r.Read())
                        yield return selector(r);
            }
        }
    }
    

    我有这个扩展方法:

    public static void Parameterize(this IDbCommand command, string name, object value)
    {
        var parameter = command.CreateParameter();
        parameter.ParameterName = name;
        parameter.Value = value;
        command.Parameters.Add(parameter);
    }
    

    所以我打电话:

    foreach(var user in Get(query, cmd => cmd.Parameterize("saved", 1), userSelector))
    {
    
    }
    

    这是完全通用的,适合任何符合 ado.net 接口的模型。 The connection and reader objects are disposed after the collection is enumerated. 无论如何填写DataTable 使用IDataAdapterFill 方法can be faster than DataTable.Load

    【讨论】:

    • 您的注释“连接对象和阅读器仅在集合被枚举一次后才被释放”让我有点困惑。我正在阅读的所有内容,包括您的句子链接的 SO 问题,都表明即使集合没有被完全枚举(例如通过打破正在使用它的foreach),它也会被处理掉。你是说它不是,还是打破迭代算作集合被“枚举一次”?顺便说一句,感谢您充实 Func 版本。
    • @bubbleking 我在那里做了一个误导性的陈述。我试图传达这样的观点,即在您枚举列表后将处理连接和读取器对象 - 如果您必须枚举 - 无论您是否跳出循环。如果您不枚举,则永远不会使用连接对象。我将编辑答案。感谢您指出。
    【解决方案5】:

    我从来都不是让数据层返回通用数据对象的忠实粉丝,因为这几乎消除了将代码分离到自己的层中的全部意义(如果接口不是,您如何切换数据层? t 定义?)。

    我认为你最好的办法是让所有这样的函数返回你自己创建的自定义对象的列表,然后在你的数据中,你将你的过程/查询调用到数据读取器中,并遍历创建列表。

    这将更容易处理(尽管最初是创建自定义类),更容易处理您的连接(因为您不会返回任何与之关联的对象),并且应该更快。唯一的缺点是所有内容都会像您提到的那样被加载到内存中,但我认为这不会引起关注(如果是,我认为需要调整查询)。

    【讨论】:

    • 我们有一个 4 层架构。数据层将通用数据对象(DataTable、DataRow、DataReader)返回到转换层,转换层将它们转换为强类型业务对象。将伤害降到最低。
    • 我明白了。我想唯一的缺点是在两个层中循环以创建列表,但我认为影响会很小。
    • IEnumerable 的优点在于您仍然只循环遍历数据一次。所以是的,影响应该是不存在的。
    猜你喜欢
    • 2013-12-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-02-27
    • 2013-09-06
    • 2011-01-23
    • 2019-02-05
    • 1970-01-01
    相关资源
    最近更新 更多