Linq: Asomándose detrás de escena

martes, 5 de febrero de 2008

Para entender un poco mejor lo que ocurre al utilizar LINQ to SQL (DLINQ), voy a mostrar con ejemplos prácticos un poco de lo que ocurre cada vez que ejecutamos un query LINQ sobre SQL

Utilizando SqlMetal o el O/R Designer, podemos crear para cada tabla existente en el servidor SQL una colección equivalente, contenida en un DataContext, que representa una vista de nuestra base de datos.

Además, estas tablas son IQueryables, lo que significan que son colecciones de elementos sobre los cuales pueden escribirse queries LINQ, Por Ejemplo:

var query = from p in VideoClubDataContext.Peliculas where p.Genero == "Terror" select p.ClienteConCopia


  






Enumerando resultados



Peliculas es un IQueryable, por lo tanto también es un IEnumerable. Pero a diferencia de una lista o array en memoria, al enumerar sobre éste, el trabajo de construir el enumerador, es delegado a un "Provider", que en el caso de LINQ to SQL, traduce la expresión C# (o VB.Net) a SQL, y enumera los resultados con un SqlCommand creado por el DataContext (quien conoce el ConnectionString necesario).



Si prestaron atención a las presentaciones de LINQ sabrán que hasta este punto, al construir mi objeto query, todavía ninguna conexión se abrió con el SQL, solo se construyó en memoria la estructura de la consulta (esto es un ExpressionTree que mencionaré más adelante).



Recién al enumerar este objeto query, se construye la sintaxis SQL, y se envía al SQL Server, trayendo los resultados para ser enumerados.




foreach (Cliente cliente in query)
EnviarMailRecordatorio(cliente.Email);


Una de las primeras dudas que tuvimos es: Qué ocurre si vuelvo a enumerar el objeto query? Existe algún tipo de caché de los resultados?



La respuesta es... no. Simplemente porque enumerar nuestro objeto query, es enumerar la consulta, con lo cual se construye nuevamente la sentencia SQL, se abre (o reutiliza) una conexión con el SQL, y se devuelven los nuevos resultados en la enumeración.



Si queremos enumerar los resultados anteriores nuevamente, podemos hacerlo de esta forma




Cliente[] clientes = query.ToArray();
foreach (Cliente cliente in clientes)
EnviarMailRecordatorio(cliente.Email);
foreach (Cliente cliente in clientes)
LlamarPidiendoDevolucionDeCopias(cliente.Nombre, cliente.Telefono);


y evitamos así viajar 2 veces al SQL.



 



El Árbol de Las Expresiones



Yendo un poco más lejos nos preguntamos que ocurre cuando los parámetros de un query se modifican luego de haberlo construido, y enumerado, dicho de otra forma, que ocurre si ejecuto el siguiente código:




txtDirector.Text = "Kubrik";

// solo por comodidad, utilizo en este query dot-notation
var query = VideoClubDataContext.Peliculas.Where(p => p.Director.StartsWith(txtDirector.Text)).Select(p => p.Titulo);

MessageBox.Show(query.ToArray()[0]);


// modifico una variable que sirve como parámetro en query
txtDirector.Text = "de la Iglesia";



// enumero nuevamente, con el método ToArray()
MessageBox.Show(query.ToArray()[0]);


 



El primer mensaje será "2001: Una Odisea del Espacio" (de Kubrik, claro).



La gran pregunta ahora es... que mostrará el segundo mensaje? Si apuestan a que el mensaje es el mismo, perdieron!



El segundo mensaje será "300 Balas" (de Alex de la Iglesia, claro)



Como se entera nuestro objeto query de que el texto de nuestro TextBox cambió? La respuesta es el Expression Tree



Un Expression Tree es un estructura en memoria que representa de forma jerárquica una expresión C# (o VB.Net), no es código compilado, de manera que permite recorrer la expresión del query, convirtiéndola a sintaxis SQL.



Expression Trees es un tema que requiere otros posts, con lo cual sin entrar en más detalles, voy a mostrar algo similar a una parte del árbol que se construye en memoria con nuestro query de más arriba:




  • LlamadaAFuncion

    • nombre = StartsWith


    • objeto = Atributo

      • nombre = "Director"


      • objeto = p (Elemento de la coleccion)




    • parametro1 = Atributo

      • nombre = "Text"


      • objeto = "txtDirector"







Es decir que el objeto query sabe que debe utilizar como parámetro el valor de la propiedad Text del objeto txtDirector, recién al enumerar el objeto query, esto se traducirá a algo como:




WHERE [t0].[Director] LIKE @p0

donde:

@p0 = 'Kubrik%'


Esto también nos permite comenzar a entender como sintaxis LINQ se traduce en código SQL, y lo que es muy importante, cuando.



Debe pensarse bien en que momento enumerarlos, y cuando utilizar ToArray(), ToList(), etc., para cachear los resultados.