jueves, 18 de agosto de 2011

Ejecución diferida en LINQ

Ahora que me estoy metiendo de lleno con LINQ, he visto que hay un concepto que es muy importante de entender y asimilar que es la ejecución aplazada o diferida (en inglés deferred).

Para percatarnos de la importancia de este asunto, empezaremos la casa por el tejado y así veremos cuál es el resultado de una ejecución aplazada en una consulta de LINQ.

Module Module1

 

    Sub Main()

 

        Dim personas As New List(Of Persona)

        personas.Add(New Persona With {.Nombre = "Sergio"})

        personas.Add(New Persona With {.Nombre = "Antonio"})

        personas.Add(New Persona With {.Nombre = "Dani"})

        personas.Add(New Persona With {.Nombre = "Carmen"})

        personas.Add(New Persona With {.Nombre = "Jimena"})

 

        Dim consulta = From p In personas Where p.Nombre.IndexOf("a") <> -1 Select p

 

        For Each persona In consulta

            Console.WriteLine(persona.Nombre)

        Next

 

        personas.Add(New Persona With {.Nombre = "Sonia"})

 

        personas.Remove(personas.Find(Function(p) p.Nombre = "Dani"))

 

        For Each persona In consulta

            Console.WriteLine(persona.Nombre)

        Next

 

        Console.Read()

 

    End Sub

 

End Module

 

Public Class Persona

    Public Property Nombre As String

End Class

 

Este código realiza las siguientes acciones:

  • Crear una lista de objetos del tipo Persona.
  • Agregar 5 personas.
  • Crear una consulta LINQ, cuya condición es válida para 3 personas (Dani, Carmen y Jimena).
  • Imprimir los resultados de la consulta.
  • Añadir a nueva persona que también cumplirá la condición de la consulta (Sonia).
  • Eliminar una persona que cumple la condición de la consulta (Dani).
  • Volver a imprimir los resultados de la consulta.

Siendo así, los resultados son:

Primera impresión

Segunda impresión

Dani

 

Carmen

Carmen

Jimena

Jimena

 

Sonia

 

Gracias (o debido a… eso ahora lo discutiremos), las dos resultados son distintos.

Es decir, hemos cambiado la lista de Personas y la consulta LINQ parece que ha sabido actualizar los cambios. ¿Es esto lo que querías? ¿Hubieras preferido que los cambios no se hubieran reflejado y seguir trabajando con los datos originales? Pues si quieres resolver estas dudas tienes que entender que es la ejecución aplazada y la ejecución inmediata y después tomar la decisión que creas oportuna según tus necesidades.

Cuando declaramos una expresión de consulta LINQ y la guardamos en una variable, por ejemplo: Dim consulta = From p In personas Where p.Nombre.IndexOf("a") <> -1 Select p, en realidad lo que estamos guardando en la variable “consulta” no son los resultados de la ejecución de la consulta LINQ sino que estamos guardando la definición de la consulta y preparando la variable para almacenar los resultados, estamos guardando lo que en LINQ se conoce como un árbol de expresión (expression tree). De hecho, si nos fijamos en el tipo de la variable “consulta” veremos que es del tipo IEnumerable(Of Persona), es decir, se ha preparado para almacenar una lista de personas.

clip_image001

Siendo así, ¿Cuándo se ejecuta entonces la consulta? Pues se ejecuta cuando accedamos a los datos por primera vez (esto puede ser por un bucle For, por la ejecución de una función de agregado, porque llamamos al método ToList o ToArray, etc.)

En nuestro caso, la consulta se ejecuta en bucle For.

Sin embargo más tarde manipulamos la lista (agregando y eliminando elementos) y en el siguiente bucle For se vuelve a ejecutar la consulta y es por ello que los resultados coinciden con el estado actual de la lista.

Lo positivo de este comportamiento es que con una sola expresión de consulta (la que guardamos en la variable “consulta”) podemos consultar varias veces la misma lista y no tener que crear una nueva consulta cada vez que cambien los datos de la lista, siempre tendremos los datos actualizados.

Lo negativo es que si no sabes que el comportamiento predeterminado de una consulta LINQ es la ejecución diferida, puedes llegar a romperte los sesos pensando de donde salen esos nuevos elementos de la lista, y peor aún, imagina que en vez de LINQ to Objects estás trabajando con LINQ to Entities (o LINQ to SQL) y entonces cada nueva ejecución de la consulta significará una nueva consulta ejecutada en el motor de base de datos subyacente… vamos un application killer si no sabes manejarlo.

Veamos cómo se comporta el depurador para seguir afianzando este tema:

clip_image002

Sólo cuando el bucle For comienza a solicitar elementos sobre los que iterar es cuando la consulta es ejecutada y el con el depurador y paso a paso por instrucciones (F11), podemos ver como la consulta es ejecutada.

Igualmente, si antes de entrar en el bucle For hubiéramos inspeccionado la variable “consulta”, veríamos que se nos invita a “Expandir para procesar la colección”. De este modo y si expandimos, se ejecutaría la consulta puesto que estamos queriendo ver resultados y para mostrarlos, lógicamente se necesita ejecutar la consulta. Pero no te engañes, en la siguiente instrucción (el bucle For) se volvería a ejecutar.

En definitiva, cada vez que cualquier instrucción, método, etc., necesite iterar sobre el resultado, se ejecutará de nuevo la consulta.

clip_image003

Si trabajamos con datos en memoria (y sobre todo si las estructuras de datos no tiene un gran número de elementos), podemos hacernos los “suecos” con este tema porque que una consulta se ejecute varias veces no supondrá una merma importante de rendimiento. Sin embargo, en un escenario de base de datos (ya sea con Linq2Sql o Linq2Entities), una consulta que se ejecuta varias veces puede suponer un cuello de botella. Además, si la aplicación es multiusuario (ya me dirás tú que aplicación con base de datos no lo es), imagina filas “nuevas” apareciendo por arte de magia en tu maravillosa colección y dando al traste con tu código. ¿Cómo solucionar esto y operar sólo sobre las filas originales?

La solución pasa por “forzar” la ejecución inmediata de la consulta. De este modo la solución pasa por llamar a los métodos ToList o ToArray, que al querer devolvernos una lista o un array tendrán que ejecutar la consulta asociada y además la variable “consulta” ya sólo almacenará los resultados (no la expresión de consulta).

        Dim consulta =

            (From p In personas

            Where p.Nombre.IndexOf("a") <> -1

            Select p).ToList()

 

A partir de aquí, cualquier cambio en la colección “personas” no será reflejado en los resultados que guarda la variable “consulta”.

En cualquier caso, la solución para lidiar con la ejecución diferida es simple y llanamente entender que ocurre y obrar en consecuencia en función de los requerimientos de nuestro código (es decir, ToList está bien pero no es la panacea).

Espero que al menos hayamos entendido lo importante que es este tema, y que si no sabemos lo que es la ejecución diferida, tarde o temprano tendremos problemas con LINQ.

Un saludo!

1 comentario:

  1. Sergio, muy interesante. Y la diferencia es clara. Creo que nos vamos a hacer amigos del .ToList y similares...

    ResponderEliminar