La connessione al database verrà chiusa se restituiamo la riga Datareader e non leggiamo tutti i record?


15

Comprendendo come yieldfunziona la parola chiave, mi sono imbattuto in link1 e link2 su StackOverflow che sostiene l'uso di yield returniterando su DataReader e soddisfa anche le mie esigenze. Ma mi chiedo cosa succede se uso yield returncome mostrato di seguito e se non eseguo iterazioni sull'intero DataReader, la connessione DB rimarrà aperta per 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;
  }
}

Ho provato lo stesso esempio in un'app console di esempio e ho notato durante il debug che il blocco di infine GetRecords()non viene eseguito. Come posso garantire quindi la chiusura di DB Connection? Esiste un modo migliore rispetto all'uso della yieldparola chiave? Sto cercando di progettare una classe personalizzata che sarà responsabile dell'esecuzione di determinati SQL e stored procedure su DB e restituirà il risultato. Ma non voglio restituire il DataReader al chiamante. Inoltre voglio assicurarmi che la connessione verrà chiusa in tutti gli scenari.

Modifica Modificata la risposta alla risposta di Ben poiché non è corretto aspettarsi che i chiamanti del metodo utilizzino correttamente il metodo e rispetto alla connessione DB sarà più costoso se il metodo viene chiamato più volte senza motivo.

Grazie Jakob e Ben per la spiegazione dettagliata.


Da quello che vedo non stai aprendo la connessione in primo luogo
paparazzo,

@Frisbee Edited :)
GawdePrasad

Risposte:


12

Sì, dovrai affrontare il problema che descrivi: fino a quando non completi l'iterazione del risultato, manterrai la connessione aperta. Ci sono due approcci generali che posso pensare di affrontare questo:

Spingi, non tirare

Attualmente stai restituendo una IEnumerable<IDataRecord>struttura di dati da cui puoi estrarre. Invece, si potrebbe passare il proprio metodo per spingere i suoi risultati fuori. Il modo più semplice sarebbe quello di passare in unAction<IDataRecord> chiamata su ogni iterazione:

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

Tieni presente che, dato che hai a che fare con una raccolta di articoli, IObservable / IObserverpotrebbe essere una struttura di dati un po 'più appropriato, ma a meno che non ne hai bisogno, una semplice Actionè molto più semplice.

Valutare con impazienza

Un'alternativa è solo assicurarsi che l'iterazione sia completamente completata prima di tornare.

Normalmente puoi farlo semplicemente inserendo i risultati in un elenco e poi restituendoli, ma in questo caso c'è l'ulteriore complicazione di ogni elemento che è lo stesso riferimento al lettore. Quindi hai bisogno di qualcosa per estrarre il risultato che ti serve dal lettore:

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;
}

Sto usando l'approccio che hai chiamato Eagerly Evaluate. Ora sarà un sovraccarico di memoria se Datareader del mio SQLCommand restituisce più output (come myReader.NextResult ()) e costruisco un elenco di elenchi? O inviare il datareader al chiamante [personalmente non lo preferisco] sarà molto efficace? [ma la connessione sarà aperta più a lungo]. Ciò che sono precisamente confuso qui è tra il compromesso di mantenere la connessione aperta più a lungo rispetto alla creazione di un elenco di elenchi. Puoi tranquillamente supporre che ci saranno quasi 500 + persone che visitano la mia pagina web che si collega a DB.
GawdePrasad,

1
@GawdePrasad Dipende da cosa farebbe il chiamante con il lettore. Se solo inserisse gli elementi in una raccolta stessa, non ci sarebbe un sovraccarico significativo. Se eseguisse un'operazione che non richiede di conservare tutti i valori in memoria, allora c'è un sovraccarico di memoria. Tuttavia, a meno che tu non stia conservando quantità molto grandi, probabilmente non è un sovraccarico di cui dovresti preoccuparti. In entrambi i casi, non ci dovrebbe essere un significativo sovraccarico di tempo.
Ben Aaronson,

In che modo l'approccio "push, non pull" risolve il problema "fino al termine dell'iterazione sul risultato, manterrai la connessione aperta"? La connessione è ancora aperta fino al termine dell'iterazione anche con questo approccio, non è vero?
BornToCode

1
@BornToCode Vedi la domanda: "se uso il rendimento restituito come mostrato di seguito e se non eseguo iterazioni sull'intero DataReader, la connessione DB rimarrà aperta per sempre". Il problema è che si restituisce un IEnumerable e ogni chiamante che lo gestisce deve essere consapevole di questo gotcha speciale: "Se non finisci di iterare su di me, terrò aperta una connessione". Nel metodo push, togli quella responsabilità al chiamante: non hanno alcuna opzione per iterare parzialmente. Quando viene restituito loro il controllo, la connessione viene chiusa.
Ben Aaronson,

1
Questa è una risposta eccellente Mi piace l'approccio push, perché se hai una grande query che stai presentando in modo paginato, puoi interrompere la lettura in anticipo per la velocità passando in Func <> piuttosto che in Action <>; questo sembra più pulito che rendere la funzione GetRecords anche il paging.
Whelkaholism

10

Il tuo finally blocco verrà sempre eseguito.

Quando si usa yield return il compilatore verrà creata una nuova classe nidificata per implementare una macchina a stati.

Questa classe conterrà tutto il codice dai finallyblocchi come metodi separati. Terrà traccia dei finallyblocchi che devono essere eseguiti in base allo stato. Tutti i finallyblocchi necessari verranno eseguiti inDispose metodo.

Secondo le specifiche del linguaggio C #, foreach (V v in x) embedded-statementviene espanso a

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

quindi l'enumeratore verrà eliminato anche se si esce dal ciclo con breako return.

Per ulteriori informazioni sull'implementazione dell'iteratore, puoi leggere questo articolo di Jon Skeet

modificare

Il problema con tale approccio è che ti affidi ai client della tua classe per usare correttamente il metodo. Potrebbero ad esempio ottenere direttamente l'enumeratore e iterarlo attraverso awhile ciclo senza eliminarlo.

Dovresti prendere in considerazione una delle soluzioni suggerite da @BenAaronson.


1
Questo è estremamente inaffidabile. Ad esempio: var records = GetRecords(); var first = records.First(); var count = records.Count()eseguirà effettivamente quel metodo due volte, aprendo e chiudendo ogni volta la connessione e il lettore. Iterate su di esso due volte fa la stessa cosa. Potresti anche passare il risultato per molto tempo prima di iterarlo, ecc. Nulla nella firma del metodo implica quel tipo di comportamento
Ben Aaronson,

2
@BenAaronson Nella mia risposta mi sono concentrato sulla domanda specifica sull'attuazione attuale. Se guardi l'intero problema, sono d'accordo che è molto meglio usare il modo che hai suggerito nella tua risposta.
Jakub Lortz,

1
@JakubLortz Abbastanza giusto
Ben Aaronson,

2
@EsbenSkovPedersen Di solito quando provi a fare qualcosa a prova di idiota, perdi alcune funzionalità. Devi bilanciarlo. Qui non perdiamo molto caricando avidamente i risultati. Comunque, il problema più grande per me è la denominazione. Quando vedo un metodo chiamato GetRecordsreturn IEnumerable, mi aspetto che carichi i record in memoria. D'altra parte, se fosse una proprietà Records, preferirei presumere che sia una sorta di astrazione sul database.
Jakub Lortz,

1
@EsbenSkovPedersen Bene, vedi i miei commenti sulla risposta di nvoigt. Non ho alcun problema in generale con i metodi che restituiscono un IEnumerable che farà un lavoro significativo quando viene ripetuto, ma in questo caso non sembra corrispondere alle implicazioni della firma del metodo
Ben Aaronson,

5

Indipendentemente dal yieldcomportamento specifico , il codice contiene percorsi di esecuzione che non elimineranno correttamente le risorse. Cosa succede se la seconda riga genera un'eccezione o la terza? O anche il tuo quarto? Avrai bisogno di una catena try / finally molto complessa, oppure puoi usare i usingblocchi.

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;
               }
            }
        }
    }
}

La gente ha osservato che questo non è intuitivo, che le persone che chiamano il tuo metodo potrebbero non sapere che enumerare il risultato due volte chiamerà il metodo due volte. Bene, sfortuna. È così che funziona la lingua. Questo è il segnale che IEnumerable<T>invia. C'è un motivo per cui questo non ritorna List<T>o T[]. Le persone che non lo conoscono devono essere educate, non aggirate.

Visual Studio ha una funzione chiamata analisi del codice statico. Puoi usarlo per scoprire se hai smaltito correttamente le risorse.


Il linguaggio consente all'iterazione IEnumerabledi fare ogni sorta di cose, come lanciare eccezioni totalmente non correlate o scrivere file da 5 GB su disco. Solo perché la lingua lo consente non significa che è accettabile restituire IEnumerableciò che lo fa da un metodo che non dà alcuna indicazione che ciò accadrà.
Ben Aaronson,

1
Restituire un IEnumerable<T> è l'indicazione che enumerarlo due volte comporterà due chiamate. Riceverai anche utili avvisi nei tuoi strumenti se ne elenchi più volte. Il punto relativo al lancio di eccezioni è ugualmente valido indipendentemente dal tipo di ritorno. Se quel metodo restituisse un List<T>, genererebbe le stesse eccezioni.
nvoigt,

Capisco il tuo punto e in una certa misura concordo sul fatto che stai consumando un metodo che ti dà un IEnumerableavvertimento allora. Ma penso anche che come autore di metodi, hai la responsabilità di aderire a ciò che implica la tua firma. Il metodo è chiamato GetRecords, quindi quando ritorna ti aspetteresti di avere i record . Se fosse chiamato GiveMeSomethingICanPullRecordsFrom(o, in modo più realistico GetRecordSourceo simile), un pigro IEnumerablesarebbe più accettabile.
Ben Aaronson,

@BenAaronson Sono d'accordo, ad esempio File, ReadLinese ReadAllLinesposso essere facilmente distinto e questa è una buona cosa. (Anche se è più dovuto al fatto che un sovraccarico del metodo non può differire solo per il tipo restituito). Forse ReadRecords e ReadAllRecords si adatterebbero anche qui come schema di denominazione.
nvoigt,

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.