A conexão com o banco de dados será fechada se produzirmos a linha do datareader e não lermos todos os registros?

15

Ao entender como a yieldpalavra-chave funciona, deparei-me com o link1 e o link2 no StackOverflow, que defendem o uso de yield returniterações no DataReader e também atendem às minhas necessidades. Mas isso me faz pensar no que acontece, se eu usar yield returncomo mostrado abaixo e se eu não percorrer todo o DataReader, a conexão com o banco de dados permanecerá aberta para sempre?

IEnumerable<IDataRecord> GetRecords()
{
    SqlConnection myConnection = new SqlConnection(@"...");
    SqlCommand myCommand = new SqlCommand(@"...", myConnection);
    myCommand.CommandType = System.Data.CommandType.Text;
    myConnection.Open();
    myReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
    try
       {               
          while (myReader.Read())
          {
            yield return myReader;          
          }
        }
    finally
        {
            myReader.Close();
        }
}


void AnotherMethod()
{
    foreach(var rec in GetRecords())
    {
       i++;
       System.Console.WriteLine(rec.GetString(1));
       if (i == 5)
         break;
  }
}

Tentei o mesmo exemplo em um exemplo de aplicativo de console e notei, durante a depuração, que o bloco finalmente GetRecords()não é executado. Como posso garantir o fechamento do DB Connection? Existe uma maneira melhor do que usar yieldpalavras-chave? Estou tentando criar uma classe personalizada que será responsável pela execução de SQLs selecionados e procedimentos armazenados no DB e retornará o resultado. Mas não quero devolver o DataReader ao chamador. Também quero ter certeza de que a conexão será fechada em todos os cenários.

Editar A resposta foi alterada para a resposta de Ben, pois é incorreto esperar que os chamadores do método usem o método corretamente e, com relação à conexão com o banco de dados, será mais caro se o método for chamado várias vezes sem motivo.

Obrigado Jakob e Ben pela explicação detalhada.

GawdePrasad
fonte
Pelo que vejo você não está abrindo a conexão em primeiro lugar
paparazzo
@Frisbee Edited :)
GawdePrasad

Respostas:

12

Sim, você enfrentará o problema que descreve: até concluir a iteração sobre o resultado, manterá a conexão aberta. Há duas abordagens gerais em que posso pensar para lidar com isso:

Empurre, não puxe

Atualmente você está retornando uma IEnumerable<IDataRecord>estrutura de dados da qual você pode extrair. Em vez disso, você poderia mudar o seu método para empurrar seus resultados fora. A maneira mais simples seria passar uma Action<IDataRecord>chamada em cada iteração:

void GetRecords(Action<IDataRecord> callback)
{
    // ...
      while (myReader.Read())
      {
        callback(myReader);
      }
}

Observe que, como você está lidando com uma coleção de itens, IObservable/ IObserverpode ser uma estrutura de dados um pouco mais apropriada, mas, a menos que você precise, um simples Actioné muito mais direto.

Avalie Ansiosamente

Uma alternativa é garantir que a iteração seja totalmente concluída antes de retornar.

Normalmente, você pode fazer isso colocando os resultados em uma lista e depois retornando, mas nesse caso há a complicação adicional de cada item sendo a mesma referência para o leitor. Então, você precisa de algo para extrair o resultado necessário do leitor:

IEnumerable<T> GetRecords<T>(Func<IDataRecord,T> extractor)
{
    // ...
     var result = new List<T>();
     try
     {
       while (myReader.Read())
       {
         result.Add(extractor(myReader));         
       }
     }
     finally
     {
         myReader.Close();
     }
     return result;
}
Ben Aaronson
fonte
Estou usando a abordagem que você nomeou Eagerly Evaluate. Agora será uma sobrecarga de memória se o Datareader do meu SQLCommand retornar várias saídas (como myReader.NextResult ()) e eu construir uma lista de lista? Ou enviar o datareader para o chamador [eu pessoalmente não prefiro] será muito eficiente? [mas a conexão ficará aberta por mais tempo]. O que estou precisamente confuso aqui é entre a desvantagem de manter a conexão aberta por mais tempo do que criar uma lista de lista. Você pode assumir com segurança que haverá mais de 500 pessoas visitando minha página da web que se conecta ao DB.
precisa saber é o seguinte
1
@ GawdePrasad Depende do que o chamador faria com o leitor. Se apenas colocar itens em uma coleção em si, não há sobrecarga significativa. Se ele executasse uma operação que não requer a manutenção de muitos valores na memória, haverá uma sobrecarga de memória. No entanto, a menos que você esteja armazenando quantidades muito grandes, provavelmente não será uma sobrecarga com a qual você deve se preocupar. De qualquer forma, não deve haver um tempo significativo de sobrecarga.
quer
Como a abordagem "empurre, não puxe" resolve o problema "até você terminar de iterar sobre o resultado, manterá a conexão aberta"? A conexão ainda é mantida aberta até você concluir a iteração, mesmo com essa abordagem, não é?
BornToCode
1
@BornToCode Veja a pergunta: "se eu usar o retorno de rendimento, como mostrado abaixo, e se eu não percorrer todo o DataReader, a conexão com o banco de dados permanecerá aberta para sempre". O problema é que você devolve um IEnumerable e todo chamador que lida com isso deve estar ciente dessa pegada especial: "Se você não terminar de repetir a operação, manterei aberta uma conexão". No método push, você tira essa responsabilidade do chamador - ele não tem opção para iterar parcialmente. Quando o controle é retornado a eles, a conexão é fechada.
Ben Aaronson
1
Esta é uma excelente resposta. Eu gosto da abordagem push, porque se você tem uma grande consulta que está apresentando de forma paginada, pode abortar a leitura anterior por velocidade, passando em Func <> em vez de em Action <>; isso parece mais limpo do que tornar a função GetRecords também fazer paginação.
Whelkaholism
10

Seu finallybloco sempre será executado.

Quando você usa yield returno compilador, cria uma nova classe aninhada para implementar uma máquina de estado.

Essa classe conterá todo o código dos finallyblocos como métodos separados. Ele acompanhará os finallyblocos que precisam ser executados dependendo do estado. Todos os finallyblocos necessários serão executados no Disposemétodo

De acordo com a especificação da linguagem C #, foreach (V v in x) embedded-statementé expandido para

{
    E e = ((C)(x)).GetEnumerator();
    try
    {
        while (e.MoveNext())
        {
            V v = (V)(T)e.Current;
            embedded - statement
        }
    }
    finally {
         // Dispose e
    }
}

portanto, o enumerador será descartado mesmo se você sair do loop com breakou return.

Para obter mais informações sobre a implementação do iterador, leia este artigo por Jon Skeet.

Editar

O problema dessa abordagem é que você confia nos clientes da sua classe para usar o método corretamente. Eles poderiam, por exemplo, obter o enumerador diretamente e iterá-lo em um whileloop sem descartá-lo.

Você deve considerar uma das soluções sugeridas por @BenAaronson.

Jakub Lortz
fonte
1
Isso é extremamente não confiável. Por exemplo: var records = GetRecords(); var first = records.First(); var count = records.Count()realmente executará esse método duas vezes, abrindo e fechando a conexão e o leitor a cada vez. Iterar duas vezes faz a mesma coisa. Você também pode passar o resultado em torno de um longo tempo antes de realmente Iterando sobre ele, etc. Nada na assinatura do método implica que tipo de comportamento
Ben Aaronson
2
@BenAaronson Concentrei-me na pergunta específica sobre a implementação atual em minha resposta. Se você olhar para todo o problema, concordo que é muito melhor usar o modo sugerido em sua resposta.
Jakub Lortz
1
@JakubLortz Fair suficiente
Ben Aaronson
2
@EsbenSkovPedersen Normalmente, quando você tenta fazer algo à prova de idiotas, perde algumas funcionalidades. Você precisa equilibrar isso. Aqui não perdemos muito, carregando ansiosamente os resultados. Enfim, o maior problema para mim é a nomeação. Quando vejo um método chamado GetRecordsreturn IEnumerable, espero que carregue os registros na memória. Por outro lado, se fosse uma propriedade Records, prefiro assumir que é algum tipo de abstração sobre o banco de dados.
Jakub Lortz
1
@EsbenSkovPedersen Bem, veja meus comentários sobre a resposta de nvoigt. Não tenho nenhum problema em geral, com métodos retornando um IEnumerable que irá fazer um trabalho significativo quando iterado, mas neste caso não parece enquadrar-se as implicações da assinatura do método
Ben Aaronson
5

Independentemente do yieldcomportamento específico , seu código contém caminhos de execução que não descartarão seus recursos corretamente. E se sua segunda linha lançar uma exceção ou sua terceira? Ou até o seu quarto? Você precisará de uma cadeia try / finalmente muito complexa, ou poderá usar usingblocos.

IEnumerable<IDataRecord> GetRecords()
{
    using(var connection = new SqlConnection(@"..."))
    {
        connection.Open();

        using(var command = new SqlCommand(@"...", connection);
        {
            using(var reader = command.ExecuteReader())
            {
               while(reader.Read())
               {
                   // your code here.
                   yield return reader;
               }
            }
        }
    }
}

As pessoas observaram que isso não é intuitivo, que as pessoas que chamam seu método podem não saber que enumerar o resultado duas vezes chamará o método duas vezes. Bem, azar. É assim que a linguagem funciona. Esse é o sinal que IEnumerable<T>envia. Há uma razão para isso não retornar List<T>ou T[]. As pessoas que não sabem disso precisam ser educadas, não contornadas.

O Visual Studio possui um recurso chamado análise de código estático. Você pode usá-lo para descobrir se você descartou os recursos corretamente.

nvoigt
fonte
O idioma permite que a iteração IEnumerablefaça todo o tipo de coisas, como lançar exceções totalmente não relacionadas ou gravar arquivos de 5 GB no disco. Só porque a linguagem permite, isso não significa que é aceitável retornar IEnumerableum método que não fornece nenhuma indicação de que isso acontecerá.
Ben Aaronson
1
Retornar um IEnumerable<T> é a indicação de que enumerá-lo duas vezes resultará em duas chamadas. Você receberá avisos úteis em suas ferramentas se enumerar várias vezes várias vezes. O ponto sobre lançar exceções é igualmente válido, independentemente do tipo de retorno. Se esse método retornasse um List<T>, lançaria as mesmas exceções.
Nvoigt 19/10/2015
Entendo seu ponto de vista e, até certo ponto, concordo - se você está consumindo um método que fornece um IEnumerableemulador de advertência. Mas também acho que, como redator de métodos, você tem a responsabilidade de aderir ao que sua assinatura implica. O método é chamado GetRecords, portanto, quando retornar, você esperaria que ele obtivesse os registros . Se fosse chamado GiveMeSomethingICanPullRecordsFrom(ou, mais realista GetRecordSourceou similar), um preguiçoso IEnumerableseria mais aceitável.
quer
@BenAaronson Eu concordo que, por exemplo File, em , ReadLinese ReadAllLinespode ser facilmente diferenciado, e isso é uma coisa boa. (Embora seja mais pelo fato de uma sobrecarga de método não diferir apenas pelo tipo de retorno). Talvez ReadRecords e ReadAllRecords também se ajustem aqui como esquema de nomes.
Nvoigt 19/10/2015
2

O que você fez parece errado, mas funcionará bem.

Você deve usar um IEnumerator <> , pois também herda IDisposable .

Mas como você está usando um foreach, o compilador ainda está usando um IDisposable para gerar um IEnumerator <> para o foreach.

de fato, o foreach envolve muita coisa internamente.

Verifique /programming/13459447/do-i-need-to-consider-disposing-of-any-ienumerablet-i-use

Avlin
fonte