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