¿Se cerrará la conexión de la base de datos si generamos la fila datareader y no leemos todos los registros?

12

Mientras entendía cómo funciona la palabra clave yield , encontré link1 y link2 en StackOverflow que aboga por el uso de yield return mientras recorre el DataReader y se adapta a mi necesidad también. Pero me pregunto qué sucede, si uso yield return como se muestra a continuación y si no repito todo el DataReader, ¿la conexión de DB se mantendrá abierta para siempre?

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

Probé el mismo ejemplo en una aplicación de consola de muestra y noté que al depurar que el bloque final de GetRecords() no se ejecuta. ¿Cómo puedo asegurar entonces el cierre de la conexión DB? ¿Hay una mejor manera de usar la palabra clave yield ? Estoy tratando de diseñar una clase personalizada que será responsable de ejecutar SQLs seleccionados y procedimientos almacenados en la base de datos y devolverá el resultado. Pero no quiero devolver el DataReader a la persona que llama. También quiero asegurarme de que la conexión se cerrará en todos los escenarios.

Editar Se modificó la respuesta a la respuesta de Ben, ya que es incorrecto esperar que las personas que llaman al método utilicen el método correctamente y con respecto a la conexión a la base de datos será más costoso si el método se llama varias veces sin costo. razón.

Gracias, Jakob y Ben por la explicación detallada.

    
pregunta GawdePrasad 19.10.2015 - 11:57

4 respuestas

11

Sí, enfrentará el problema que describe: hasta que termine de iterar sobre el resultado, mantendrá la conexión abierta. Hay dos enfoques generales que se me ocurren para lidiar con esto:

Empuje, no tire

Actualmente estás devolviendo IEnumerable<IDataRecord> , una estructura de datos de la que puedes obtener datos. En su lugar, puede cambiar su método para empujar sus resultados. La forma más sencilla sería pasar un Action<IDataRecord> al que se llama en cada iteración:

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

Tenga en cuenta que dado que está tratando con una colección de elementos, IObservable / IObserver podría ser una estructura de datos un poco más apropiada, pero a menos que lo necesite, un simple Action es mucho más directo. p>

Evalúa con entusiasmo

Una alternativa es simplemente asegurarse de que la iteración se complete por completo antes de regresar.

Normalmente puedes hacer esto simplemente colocando los resultados en una lista y luego devolviéndolo, pero en este caso existe la complicación adicional de que cada elemento sea la misma referencia para el lector. Entonces necesitas algo para extraer el resultado que necesitas del lector:

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;
}
    
respondido por el Ben Aaronson 19.10.2015 - 14:12
10

Tu bloque finally siempre se ejecutará.

Cuando usa yield return , el compilador creará una nueva clase anidada para implementar una máquina de estado.

Esta clase contendrá todo el código de finally blocks como métodos separados. Mantendrá un seguimiento de los bloques finally que deben ejecutarse según el estado. Todos los bloques finally necesarios se ejecutarán en el método Dispose .

De acuerdo con la especificación del lenguaje C #, foreach (V v in x) embedded-statement se expande a

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

por lo que el enumerador se eliminará incluso si sale del bucle con break o return .

Para obtener más información sobre la implementación de iteradores, puedes leer este este artículo de Jon Skeet

Editar

El problema con este enfoque es que confía en que los clientes de su clase usen el método correctamente. Por ejemplo, podrían obtener el enumerador directamente e iterarlo en un bucle while sin eliminarlo.

Debería considerar una de las soluciones sugeridas por @BenAaronson.

    
respondido por el Jakub Lortz 19.10.2015 - 13:45
3

Independientemente del comportamiento específico de yield , su código contiene rutas de ejecución que no dispondrán de sus recursos correctamente. ¿Qué pasa si su segunda línea lanza una excepción o su tercera? ¿O incluso tu cuarto? Necesitará una cadena de intentos / finalmente muy compleja, o puede usar using blocks.

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 comentado que esto no es intuitivo, que las personas que llaman a su método pueden no saber que la enumeración del resultado dos veces llamará al método dos veces. Bueno, mala suerte. Así es como funciona el lenguaje. Esa es la señal que envía IEnumerable<T> . Hay una razón por la que esto no devuelve List<T> o T[] . Las personas que no saben esto necesitan ser educadas, no trabajadas.

Visual Studio tiene una característica llamada análisis de código estático. Puede usarlo para averiguar si ha eliminado los recursos correctamente.

    
respondido por el nvoigt 19.10.2015 - 16:59
2

Lo que hiciste parece mal, pero funcionará bien.

Debes usar un IEnumerator < > , ya que también hereda IDisposable .

Pero debido a que está utilizando un foreach, el compilador sigue usando un IDisposible para generar un IEnumerator < > para el foreach.

En los hechos, el foreach involucra muchas cosas internamente.

Marque enlace

    
respondido por el Avlin 19.10.2015 - 13:35

Lea otras preguntas en las etiquetas