Возвращение DataReader из DataLayer в инструкции Using

У нас есть много кода уровня данных, который следует за этой общей схемой:

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 напрямую, соединение будет отключено при выходе из блока использования.

Как я могу улучшить это, чтобы позволить возвращать одну строку за раз?

Ответ 1

Еще раз, акт составления моих мыслей по этому вопросу раскрывает ответ. В частности, последнее предложение, в котором я написал "по одной строке за раз". Я понял, что мне все равно, что это 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 в бизнес-объекты в цикле. В противном случае могут возникнуть проблемы с ленивой итерацией, так что каждый раз вы увидите последний объект в запросе.

Ответ 2

Что вы хотите - это поддерживаемый шаблон, вам нужно будет использовать

cmd.ExecuteReader(CommandBehavior.CloseConnection);

и удалите оба using(), чтобы создать метод GetSomeData(). Предохранитель исключений должен быть предоставлен вызывающим абонентом, гарантируя закрытие на считывателе.

Ответ 3

В такие моменты я нахожу, что лямбды могут быть очень полезны. Подумайте об этом, вместо слоя данных, дающего нам данные, дадим слой данных нашим методом обработки данных:

public void GetSomeData(string filter, Action<IDataReader> processor)
{
    ...

    using (IDataReader reader = cmd.ExecuteReader())
    {
        processor(reader);
    }
}

Тогда бизнес-уровень будет называть его:

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

Ответ 4

Ключ yield.

Подобно оригинальному ответу Джоэля, немного больше конкретизировалось:

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. Объекты соединения и чтения расположены после перечисления коллекции. В любом случае заполнение DataTable с помощью метода IDataAdapter Fill может быть быстрее, чем DataTable.Load

Ответ 5

Я никогда не был большим поклонником того, чтобы слой данных возвращал общий объект данных, поскольку это в значительной степени растворяет всю суть того, что код разделен на свой собственный уровень (как вы можете отключить слои данных, если интерфейс isn ' t определено?).

Я считаю, что лучше всего для всех таких функций вернуть список настраиваемых объектов, которые вы создаете сами, а в ваших данных позже вы вызываете свою процедуру/запрос в datareader и повторяете это, создавая список.

Это упростит работу в целом (несмотря на начальное время создания пользовательских классов), упрощает обработку вашего соединения (поскольку вы не будете возвращать какие-либо связанные с ним объекты), и должно быть быстрее. Единственный недостаток - все будет загружено в память, как вы упомянули, но я бы не подумал, что это будет причиной беспокойства (если бы это было так, я бы подумал, что запрос нужно будет скорректировать).