В то время как на IDataReader.Read не работает с возвратом доходности, но foreach on reader делает
Это широко распространенный шаблон ADO.NET для извлечения данных из базы данных с помощью устройства чтения данных, но, как ни странно, он не работает.
Не работает:
public static IEnumerable<IDataRecord> SelectDataRecord<T>(string query, string connString)
where T : IDbConnection, new()
{
using (var conn = new T())
{
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = query;
cmd.Connection.ConnectionString = connString;
cmd.Connection.Open();
using (var reader = (DbDataReader)cmd.ExecuteReader())
{
// the main part
while (reader.Read())
{
yield return (IDataRecord)reader;
}
}
}
}
Это работает:
public static IEnumerable<IDataRecord> SelectDataRecord<T>(string query, string connString)
where T : IDbConnection, new()
{
using (var conn = new T())
{
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = query;
cmd.Connection.ConnectionString = connString;
cmd.Connection.Open();
using (var reader = (DbDataReader)cmd.ExecuteReader())
{
// the main part
foreach (var item in reader.Cast<IDataRecord>())
{
yield return item;
}
}
}
}
Единственное соответствующее изменение, которое я вижу, это то, что в первом коде итератор возвращается из цикла while
, а во втором - из цикла foreach
.
Я называю это следующим образом:
// I have to buffer for some reason
var result = SelectDataRecord<SQLiteConnection>(query, connString).ToList();
foreach(var item in result)
{
item.GetValue(0); // explosion
}
Я пробовал с SQLite коннектор .NET, а также MySQL. Результат тот же, то есть первый подход сбой, второй успешный.
Exception
SQLite
Необработанное исключение типа "Исправление System.InvalidOperationException" произошло в System.Data.SQLite.dll. Дополнительная информация: Нет текущей строки
MySQL
Необработанное исключение типа "System.Exception" произошло в MySql.Data.dll. Дополнительная информация: Нет текущего запроса в считывателе данных
Это из-за различий в реализации между reader.Read
и reader.GetEnumerator
в конкретных соединителях ADO.NET? Я не видел заметной разницы, когда я проверил источник проекта System.Data.SQLite
, GetEnumerator
вызывает Read
внутренне. В идеале я предполагаю, что в обоих случаях ключевое слово yield
предотвращает нетерпеливое выполнение метода, и циклы должны выполняться только после того, как перечисление перечислили извне.
Обновление:
Я использую этот шаблон как безопасный (по существу такой же, как и второй подход, но немного менее подробный),
using (var reader = cmd.ExecuteReader())
foreach (IDataRecord record in reader as IEnumerable)
yield return record;
Ответы
Ответ 1
Различие между двумя примерами состоит в том, что foreach
имеет различную семантику из while
, которая представляет собой простой цикл. Ниже приведено значение GetEnumerator
of foreach
.
Как говорит Джоэл, в первом примере, тот же объект-читатель получает на каждой итерации цикла while
. Это связано с тем, что оба IDataReader
, а также IDataRecord
здесь одинаковы, что является неудачным. Когда в результирующей последовательности вызывается a ToList
, завершение завершается, после чего блоки using
закрывают объекты чтения и соединения, и вы получаете список расположенных объектов-читателей той же ссылки.
Во втором примере, устройство чтения foreach
обеспечивает получение копии IDataRecord
. GetEnumerator
выполняется следующим образом:
public IEnumerator GetEnumerator()
{
return new DbEnumerator(this); // the same in MySQL as well as SQLite ADO.NET connectors
}
где MoveNext
класса System.Data.Common.DbEnumerator
реализуется следующим образом:
IDataRecord _current;
public bool MoveNext() // only the essentials
{
if (!this._reader.Read())
return false;
object[] objArray = new object[_schemaInfo.Length];
this._reader.GetValues(objArray); // caching into obj array
this._current = new DataRecordInternal(_schemaInfo, objArray); // a new copy made here
return true;
}
DataRecordInternal
- это фактическая реализация IDataRecord
, которая получается из foreach
, которая не является той же ссылкой, что и читатель, но кэшированной копией всех значений строки/записи.
System.Linq.Cast
в этом случае является простым сохранением представления, которое ничего не делает для общего эффекта. Cast<T>
будет реализован следующим образом:
public static IEnumerable<T> Cast<T>(this IEnumerable source)
{
foreach (var item in source)
yield return (T)item; // representation preserving since IDataReader implements IDataRecord
}
Можно показать пример без вызова Cast<T>
, чтобы не выявить эту проблему.
using (var reader = cmd.ExecuteReader())
foreach (var record in reader as IEnumerable)
yield return record;
Приведенный выше пример просто отлично работает.
Важным отличием является то, что первый пример проблематичен, только если вы не используете значения, считанные из базы данных, в его первом перечислении. Это только последующие перечисления, которые бросаются, поскольку к тому времени читатель будет удален. Например,
using (var reader = cmd.ExecuteReader())
while (reader.Read())
yield return reader;
...
foreach(var item in ReaderMethod())
{
item.GetValue(0); // runs fine
}
...
foreach(var item in ReaderMethod().ToList())
{
item.GetValue(0); // explosion
}
Ответ 2
Это не while
vs foreach
, что делает разницу. Это вызов .Cast<T>()
.
В первом примере вы получаете один и тот же объект на каждой итерации цикла while. Если вы не будете осторожны, вы закончите заполнение итератора производительности до фактического использования данных, и DataReader уже будет удален. Это может произойти, если вы хотите, например, вызвать .ToList()
после вызова этого метода. Лучшее, на что вы могли надеяться, было бы для каждой записи в списке иметь такое же значение.
(Pro tip: большую часть времени вы не хотите вызывать .ToList()
, пока вам не понадобится. Лучше просто работать с записями IEnumerable).
Во втором примере, когда вы вызываете .Cast<T>()
в datareader, вы фактически делаете копию данных по мере их повторения через каждую запись. Теперь вы больше не уступаете одному и тому же объекту.