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

15

Mientras que la comprensión de cómo yieldfunciona la palabra clave, me encontré con Link1 y Link2 en StackOverflow que aboga por el uso de yield returntiempo interactuando sobre el DataReader y se adapta a mi necesidad también. Pero me pregunto qué sucede si uso lo yield returnque se muestra a continuación y si no repito todo el DataReader, ¿la conexión de la base de datos permanecerá 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;
  }
}

Intenté el mismo ejemplo en una aplicación de consola de ejemplo y noté durante la depuración que el bloque finalmente GetRecords()no se ejecuta. ¿Cómo puedo asegurar el cierre de DB Connection? ¿Hay una mejor manera que usar yieldpalabras clave? Estoy tratando de diseñar una clase personalizada que se encargará de ejecutar SQL 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 Cambió la respuesta a la respuesta de Ben, ya que es incorrecto esperar que quienes llaman al método usen el método correctamente y con respecto a la conexión de la base de datos, será más costoso si el método se llama varias veces sin razón alguna.

Gracias Jakob y Ben por la explicación detallada.

GawdePrasad
fuente
Por lo que veo, no estás abriendo la conexión en primer lugar
paparazzo
@Frisbee Editado :)
GawdePrasad

Respuestas:

12

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

Empujar, no tirar

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

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

Tenga en cuenta que dado que se trata de una colección de elementos, IObservable/ IObserverpodría ser una estructura de datos ligeramente más apropiada, pero a menos que la necesite, una simple Actiones mucho más sencilla.

Evaluar con entusiasmo

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

Normalmente puede 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 necesita algo para extraer el resultado que necesita 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;
}
Ben Aaronson
fuente
Estoy usando el enfoque que nombraste Evaluar con entusiasmo. ¿Ahora será una sobrecarga de memoria si el Datareader de mi SQLCommand devuelve múltiples salidas (como myReader.NextResult ()) y construyo una lista de la lista? ¿O enviar el lector de datos a la persona que llama [personalmente no lo prefiero] será mucho más eficiente? [pero la conexión estará abierta por más tiempo]. Lo que estoy precisamente confundido aquí es entre el compromiso de mantener la conexión abierta por más tiempo que la creación de una lista de listas. Puede suponer con seguridad que habrá más de 500 personas visitando mi página web que se conecta a DB.
GawdePrasad
1
@GawdePrasad Depende de lo que haga la persona que llama con el lector. Si solo pusiera elementos en una colección en sí, no hay gastos generales significativos. Si haría una operación que no requiere mantener todos los valores en la memoria, entonces hay una sobrecarga de memoria. Sin embargo, a menos que esté almacenando cantidades muy grandes, probablemente no sea un gasto general que deba preocuparle. De cualquier manera, no debería haber un tiempo de sobrecarga significativo.
Ben Aaronson
¿Cómo el enfoque de "empujar, no tirar" resuelve el problema de "hasta que termine de iterar sobre el resultado, mantendrá la conexión abierta"? La conexión aún se mantiene abierta hasta que termine de iterar incluso con este enfoque, ¿no es así?
BornToCode
1
@BornToCode Consulte la pregunta: "si uso el rendimiento de rendimiento como se muestra a continuación y si no repito todo el DataReader, la conexión de la base de datos permanecerá abierta para siempre". El problema es que devuelve un IEnumerable y cada persona que llama que lo maneja debe tener en cuenta este problema especial: "Si no termina de iterar sobre mí, mantendré abierta una conexión". En el método push, le quitas esa responsabilidad a la persona que llama, ya que no tienen la opción de iterar parcialmente. Cuando se les devuelve el control, la conexión está cerrada.
Ben Aaronson
1
Esta es una excelente respuesta. Me gusta el enfoque de inserción, porque si tiene una gran consulta que está presentando de forma paginada, puede abortar la lectura anterior por velocidad pasando un Func <> en lugar de una Acción <>; esto se siente más limpio que hacer que la función GetRecords también haga paginación.
Whelkaholism
10

Su finallybloqueo siempre se ejecutará.

Cuando use yield returnel compilador creará una nueva clase anidada para implementar una máquina de estados.

Esta clase contendrá todo el código de los finallybloques como métodos separados. Realizará un seguimiento de los finallybloques que deben ejecutarse según el estado. Todos los finallybloques necesarios se ejecutarán en el Disposemétodo.

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

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

entonces el enumerador se eliminará incluso si sale del bucle con breako return.

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

Editar

El problema con este enfoque es que confía en los clientes de su clase para usar el método correctamente. Podrían, por ejemplo, obtener el enumerador directamente e iterarlo en un whilebucle sin desecharlo.

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

Jakub Lortz
fuente
1
Esto es extremadamente poco confiable. Por ejemplo: en var records = GetRecords(); var first = records.First(); var count = records.Count()realidad ejecutará ese método dos veces, abriendo y cerrando la conexión y el lector cada vez. Iterarlo dos veces hace lo mismo. También puede pasar el resultado durante mucho tiempo antes de iterar sobre él, etc. Nada en la firma del método implica ese tipo de comportamiento
Ben Aaronson
2
@BenAaronson Me concentré en la pregunta específica sobre la implementación actual en mi respuesta. Si observa todo el problema, estoy de acuerdo en que es mucho mejor usar la forma que sugirió en su respuesta.
Jakub Lortz
1
@JakubLortz Bastante justo
Ben Aaronson
2
@EsbenSkovPedersen Por lo general, cuando intentas hacer algo a prueba de idiotas, pierdes algo de funcionalidad. Necesitas equilibrarlo. Aquí no perdemos mucho cargando ansiosamente los resultados. De todos modos, el mayor problema para mí es el nombre. Cuando veo un método llamado GetRecordsreturn IEnumerable, espero que cargue los registros en la memoria. Por otro lado, si fuera una propiedad Records, preferiría asumir que es algún tipo de abstracción sobre la base de datos.
Jakub Lortz
1
@EsbenSkovPedersen Bueno, vea mis comentarios sobre la respuesta de nvoigt. No tengo ningún problema en general con los métodos que devuelven un IEnumerable que hará un trabajo significativo cuando se repite, pero en este caso no parece encajar con las implicaciones de la firma del método
Ben Aaronson
5

Independientemente del yieldcomportamiento específico , su código contiene rutas de ejecución que no eliminarán sus recursos correctamente. ¿Qué pasa si su segunda línea arroja una excepción o su tercera? ¿O incluso tu cuarto? Necesitará una cadena de prueba / finalmente muy compleja, o puede usar usingbloques.

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 enumerar el resultado dos veces lo llamará dos veces. Bueno, mala suerte. Así funciona el lenguaje. Esa es la señal que IEnumerable<T>envía. Hay una razón por la cual esto no regresa List<T>o T[]. Las personas que no saben esto necesitan ser educadas, no evitadas.

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

nvoigt
fuente
El lenguaje permite iterar IEnumerablepara hacer todo tipo de cosas, como lanzar excepciones totalmente no relacionadas o escribir archivos de 5GB en el disco. El hecho de que el lenguaje lo permita no significa que sea aceptable devolver un IEnumerablemétodo que lo hace desde un método que no indica que eso sucederá.
Ben Aaronson
1
Devolver un IEnumerable<T> es la indicación de que enumerarlo dos veces resultará en dos llamadas. Incluso recibirá advertencias útiles en sus herramientas si enumera un número enumerable varias veces. El punto sobre lanzar excepciones es igualmente válido independientemente del tipo de retorno. Si ese método devolviera un List<T>, arrojaría las mismas excepciones.
nvoigt
Entiendo tu punto y, hasta cierto punto, estoy de acuerdo, si estás consumiendo un método que te da una IEnumerableadvertencia. Pero también creo que, como escritor de métodos, tiene la responsabilidad de cumplir con lo que implica su firma. Se llama al método GetRecords, por lo que cuando regrese, esperaría que obtuviera los registros . Si se llamara GiveMeSomethingICanPullRecordsFrom(o, de manera más realista GetRecordSourceo similar), un perezoso IEnumerablesería más aceptable.
Ben Aaronson
@BenAaronson Estoy de acuerdo en que, por ejemplo File, ReadLinesy ReadAllLinesse puede distinguir fácilmente y eso es algo bueno. (Aunque es más debido al hecho de que una sobrecarga de método no puede diferir solo por tipo de retorno). Quizás ReadRecords y ReadAllRecords también encajarían aquí como esquema de nomenclatura.
nvoigt
2

Lo que hiciste parece estar mal, pero funcionará bien.

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

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

de hecho, el foreach involucra muchas cosas internamente.

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

Avlin
fuente