在Using语句中从Data Layer返回DataReader

时间:2009-05-11 21:02:28

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

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

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块时会切断连接。

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

5 个答案:

答案 0 :(得分:13)

再次,为问题撰写我的想法的行为揭示了答案。具体来说,我写的“一次一行”的最后一句话。我意识到我并不在乎它是一个datareader,只要我可以逐行枚举它。这引出了我的想法:

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<T>返回类型并添加Func<IDataRecord, T>参数以将DataReader状态转换为业务对象,那么我是最好的。环。否则,延迟迭代可能会出现问题,因此您每次都会在查询中看到最后一个对象。

答案 1 :(得分:7)

您想要的是支持的模式,您必须使用

cmd.ExecuteReader(CommandBehavior.CloseConnection);

并从GetSomeData()方法中删除using()。呼叫者必须提供例外安全保障,以保证读者关闭。

答案 2 :(得分: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())
        {
            ...
        }
    });

答案 3 :(得分:2)

关键是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的{​​{1}}方法填充IDataAdapter can be faster than DataTable.Load

答案 4 :(得分:0)

我从来不是让数据层返回通用数据对象的忠实粉丝,因为这几乎解决了将代码分离到自己的层中的问题(如果接口不是,那么如何切换数据层?定义了?)。

我认为最好的办法是让所有这样的函数返回您自己创建的自定义对象列表,稍后在数据中,将您的过程/查询调用到datareader中,然后遍历创建列表的那些。 / p>

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