Если мы не используем EF (такое случается), то нам нужно как-то устроить загрузку объектов из базы данных. Вариант: берём DataSet
, делаем ему SomeDataAdapter.fill(...)
, а из него берём данные для строительства нужных объектов. При этом класс, который умеет заполнять DataSet
, не знает, для объектов какого класса он это делает. Абстракция, низкая связанность, всё хорошо.
Однако, мы ждём, пока заполнится DataSet
, только после этого можем начать отправку ответа клиенту.
Ранее в публикации JSON-сериализация асинхронного стрима мы рассмотрели способ, как отправлять данные по мере поступления с помощью SomeDataReader
(например, OracleDataReader
). Там был приведён пример:
private async IAsyncEnumerable<ICatForListing> GetCatsList()
{
using OracleConnection conn = GetConnection();
var taskOpenConnection = conn.OpenAsync();
OracleCommand command = conn.CreateCommand();
command.CommandText = "select STRING_ID, INT_ID, breed, name from T_CATS";
await taskOpenConnection;
OracleDataReader dataReader = (OracleDataReader) await command.ExecuteReaderAsync();
while (true)
{
Task<bool> canRead = dataReader.ReadAsync();
Cat item = new();
item.Id = new StringIntId();
if(! await canRead)
{
yield break;
}
item.Id.StringId = dataReader["STRING_ID"].ToString()!.Trim();
item.Id.IntId = int.Parse(dataReader["INT_ID"].ToString()!.Trim());
item.Breed = dataReader["BREED"].ToString()!.Trim();
item.Name = dataReader["NANE"].ToString()!.Trim();
yield return item;
}
}
...
public async Task GetCats(HttpContext httpContext)
{
await httpContext.Response.WriteAsJsonAsync(
GetCatsList(),
new JsonSerializerOptions { PropertyNameCaseInsensitive = false }
);
}
И так делать, конечно, некрасиво. Потому что в таком случае у нас класс, который умеет работать с базой данных зависит от интерфейса ICatForListing
и класса Cat
, относящихся к бизнес-модели.
Можно попробовать получить абстрактный DbDataReader и крутить его уже на более высоком уровне:
// низкоуровневый класс, работающий с базой данных
public class Database
{
public async Task<DbDataReader> GetCatsDataReader()
{
using OracleConnection conn = GetConnection();
var taskOpenConnection = conn.OpenAsync();
OracleCommand command = conn.CreateCommand();
command.CommandText = "select STRING_ID, INT_ID, breed, name from T_CATS";
await taskOpenConnection;
return (DbDataReader) await command.ExecuteReaderAsync();
}
}
// контроллер ASP.NET
public class CatsController
{
private Database _database = new();
private async IAsyncEnumerable<ICatForListing> GetCatsList()
{
using DbDataReader dataReader = await _database.GetCatsDataReader();
while (true)
{
Task<bool> canRead = dataReader.ReadAsync();
Cat item = new();
item.Id = new StringIntId();
if(! await canRead)
{
yield break;
}
item.Id.StringId = dataReader["STRING_ID"].ToString()!.Trim();
item.Id.IntId = int.Parse(dataReader["INT_ID"].ToString()!.Trim());
item.Breed = dataReader["BREED"].ToString()!.Trim();
item.Name = dataReader["NANE"].ToString()!.Trim();
yield return item;
}
}
...
public async Task GetCats(HttpContext httpContext)
{
await httpContext.Response.WriteAsJsonAsync(
GetCatsList(),
new JsonSerializerOptions { PropertyNameCaseInsensitive = false }
);
}
}
Оказывается, это не работает:
System.InvalidOperationException : Invalid attempt to call Read when reader is closed.
Это из-за using
:
using OracleConnection conn = GetConnection();
При выходе из метода соединение закрывается, так как вызывается Dispose()
. Придётся using
убрать:
// низкоуровневый класс, работающий с базой данных
public class Database
{
public async Task<DbDataReader> GetCatsDataReader()
{
/*using*/ OracleConnection conn = GetConnection();
var taskOpenConnection = conn.OpenAsync();
OracleCommand command = conn.CreateCommand();
command.CommandText = "select STRING_ID, INT_ID, breed, name from T_CATS";
await taskOpenConnection;
return (DbDataReader) await command.ExecuteReaderAsync();
}
}
Теперь котики выводятся, но у нас висит незакрытое соединение, что также некрасиво.
И тут нас посещает идея: а давайте возвращать тот же DbDataReader
, но не целиком, а на каждой строке данных через IAsyncEnumerator<DbDataReader>
! Это означает, что мы останемся внутри метода GetCatsDataReader() до конца данных, соединение закроется правильно и наша первоначальная цель также будет достигнута. Итак:
// низкоуровневый класс, работающий с базой данных
public class Database
{
public async IAsyncEnumerable<DbDataReader> GetCatsDataReader()
{
using OracleConnection conn = GetConnection();
var taskOpenConnection = conn.OpenAsync();
OracleCommand command = conn.CreateCommand();
command.CommandText = "select STRING_ID, INT_ID, breed, name from T_CATS";
await taskOpenConnection;
using OracleDataReader dr = (OracleDataReader) await cmd.ExecuteReaderAsync();
while (await dr.ReadAsync())
{
yield return (DbDataReader)dr;
}
}
}
// контроллер ASP.NET
public class CatsController
{
private Database _database = new();
private async IAsyncEnumerable<ICatForListing> GetCatsList()
{
await foreach (DbDataReader dataReader in GetCatsDataReader())
{
Cat item = new();
item.Id = new StringIntId();
item.Id.StringId = dataReader["STRING_ID"].ToString()!.Trim();
item.Id.IntId = int.Parse(dataReader["INT_ID"].ToString()!.Trim());
item.Breed = dataReader["BREED"].ToString()!.Trim();
item.Name = dataReader["NANE"].ToString()!.Trim();
yield return item;
}
}
...
public async Task GetCats(HttpContext httpContext)
{
await httpContext.Response.WriteAsJsonAsync(
GetCatsList(),
new JsonSerializerOptions { PropertyNameCaseInsensitive = false }
);
}
}
Проверено, работает!
fedorro
Возвращать из GetCatsDataReader MyDbDataReader, в котором есть ссылки на DbDataReader и на Connection, и в его Dispose закрывать и то и другое.