Вернуть DataReader из DataLayer в операторе Using - PullRequest
10 голосов
/ 12 мая 2009

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

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

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

Ответы [ 5 ]

13 голосов
/ 12 мая 2009

Еще раз, акт составления моих мыслей на вопрос раскрывает ответ. В частности, последнее предложение, где я написал «по одной строке за раз». Я понял, что мне действительно все равно, что это устройство чтения данных, если я могу перечислять его построчно. Это привело меня к этому:

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

7 голосов
/ 12 мая 2009

То, что вы хотите, является поддерживаемым шаблоном, вам придется использовать

cmd.ExecuteReader(CommandBehavior.CloseConnection);

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

3 голосов
/ 12 мая 2009

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

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

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

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

GetSomeData("my filter", (IDataReader reader) => 
    {
        while (reader.Read())
        {
            ...
        }
    });
2 голосов
/ 14 февраля 2013

Ключом является 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

0 голосов
/ 12 мая 2009

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

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

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

...