lunes, 22 de agosto de 2011

AsEnumerable en LINQ

Como ya sabrás, en LINQ se distinguen dos tipos distintos de proveedores:
los proveedores locales y proveedores remotos.

Los proveedores locales trabajan sobre datos en memoria y en esta categoría podemos encontrar a LINQ to Objects, LINQ to XML o LINQ to DataSets.

Por otro lado, los proveedores remotos son aquellos que necesitan traducir las expresiones de consulta de LINQ en llamadas específicas a la API del origen de datos. Por ejemplo, los proveedores LINQ to SQL o LINQ to Entities tienen que traducir las expresiones de consulta LINQ a T-SQL para ejecutarlas sobre el servidor de base de datos.

Siendo así, es responsabilidad del proveedor remoto intentar abarcar el mayor número de traducciones posibles desde nuestro código VB o C# al lenguaje nativo del origen de datos, pero por supuesto no todo el monte es orégano.

Si nos centramos en LINQ to Entities podemos observar como SÍ es capaz de traducir CType(<Campo> As Integer), pero NO Convert.ToInt32(<Campo>) y lanza un error del tipo NotSupportedException (lo que viene siendo que no es capaz de encontrar una equivalencia entre Convert.ToInt32 y una función de T-SQL).

Dim t =

From r In c.Clientes

Where Convert.ToInt32(r.IdCliente) > 1

image

Sin embargo, este otro código sí es perfectamente válido:

Dim t =

From r In c.Clientes

Where CType(r.IdCliente, Integer) > 1


Y generará la siguiente instrucción SQL:

SELECT

[Extent1].[IdCliente] AS [IdCliente],

[Extent1].[Nombre] AS [Nombre]

FROM [dbo].[Clientes] AS [Extent1]

WHERE CAST( [Extent1].[IdCliente] AS int) > 1

 

Partiendo de esta base, está claro que habrá métodos de VB o C# que no serán soportados por el proveedor de LINQ y lanzarán excepciones en tiempo de ejecución, y además me parece a mí que la solución pasa por el método ensayo-error para saber qué métodos .NET soporta y cuales no (tampoco le vamos a pedir las estrellas a LINQ, ¿no?). También ten en cuenta que la excepción sucederá en tiempo de ejecución porque en tiempo de compilación no hay problema ya que aún no se ha intentado traducir la consulta LINQ al origen de datos remoto, ¡cuidado con esto!).

Si llegamos a la situación en que tenemos que llamar a ciertos métodos que no están soportados por nuestro proveedor de LINQ, sólo tenemos la solución de utilizar LINQ to Objects. Es decir, aunque estemos utilizando LINQ to SQL o LINQ to Entities, eso no significa que no podamos utilizar también LINQ to Objects. De hecho, lo más habitual es mezclar ambos para conseguir tener lo mejor de ambos proveedores.

Por un lado, está claro que utilizar el proveedor de LINQ para SQL supondrá una mejora en el rendimiento de nuestro código, puesto que atacamos directamente a la base de datos y sólo recuperamos en memoria la información necesaria. Pero por otro lado, cuando hayamos recuperado esa información y ya esté cargada en memoria nadie nos impide utilizar LINQ to Objects para transformar o manipular aún más esa información.

Siendo así, para pasar del proveedor de LINQ remoto de turno a LINQ to Objects, podemos utilizar los siguientes métodos (entre otros):

  • ToArray
  • ToList
  • ToDictionary
  • AsEnumerable

La gran diferencia entre estos métodos es que los 3 primeros fuerzan la ejecución inmediata de la consulta, mientras que AsEnumerable NO fuerza esa ejecución.

Antes de continuar, permíteme que hablemos un poco sobre IQueryable(Of T).

LINQ to SQL y LINQ to Entities trabajan con la interfaz IQueryable(Of T) que hereda de IEnumerable(Of T).

Mientras que IEnumerable(Of T) trabaja directamente sobre la secuencia en memoria a través de LINQ to Objects, IQueryable(Of T) produce árboles de expresión (expression tree) que el proveedor de LINQ remoto examinará para intentar traducir la consulta al lenguaje nativo del origen de datos.

Es importante entender (al menos conceptualmente) que es exactamente una árbol de expresión y porqué lo utiliza IQueryable(Of T).

“Un árbol de expresión contiene la definición de una serie de instrucciones que servirán para traducirlas, posteriormente, en el lenguaje nativo del origen de datos“.

Uff, espero haberme explicado bien porque ha sido una interpretación libre y quizás, no muy científica.

Siendo así, podemos ver como la interfaz IEnumerable(Of T) recibe delegados genéricos de tipo Func, mientras que IQueryable(Of T) recibe árboles de expresión del tipo Expression. En lenguaje llano, IEnumerable(Of T) recibe una operación sobre la secuencia, mientras que IQueryable(Of T) recibe que se debería hacer sobre la secuencia (pero no se hace en este momento). Así después y cuando finalmente IQueryable(Of T) quiera ejecutar la expresión de consulta, podrá recorrer todo el árbol de expresión almacenado y traducirlo a una instrucción T-SQL (por cierto, que la traducción desde árboles de expresión a código nativo del origen de datos, es llevaba a cabo por clases auxiliares denominadas “proveedores de consultas” o “query providers”, ahí queda eso como cultura popular…)

Llegados a este punto y retornando a el motivo original del post, cuando llamamos por ejemplo a ToList, estamos forzando la ejecución inmediata de la consulta y convirtiendo un objeto del tipo IQueryable(Of T) en un List(Of T). Mientras que si llamamos a AsEnumerable, simplemente estamos convirtiendo un objeto del tipo IQueryable(Of T) a IEnumerable(Of T), pero NO estamos forzando la ejecución inmediata de la consulta de LINQ.

Además, es una buena convención el utilizar AsEnumerable en nuestro código para mostrar que estamos pasando del mundo del proveedor de LINQ “de turno” a LINQ to Objects porque necesitamos a este último para implementar ciertas consultas.

Por otro lado, AsEnumerable tiene su método opuesto que es AsQueryable, que justamente hace lo contrario, es decir, convertir desde IEnumerable(Of T) a IQueryable(Of T), es decir, volver desde LINQ to Objects a LINQ to SQL o LINQ to Entities.

AsQueryable podría parecer absurdo, pero visita el siguiente enlace y verás que tiene su utilidad.

Finalmente, la recomendación para obtener lo mejor de ambos mundos (proveedor de LINQ remoto y LINQ to Objects) es:

  • Primero intentar llevar a cabo el máximo de consulta en el proveedor de LINQ remoto que estemos utilizando.
  • Después llamar a AsEnumerable.
  • Por último, utilizar LINQ to Objects para incluir el resto de condiciones en la consulta.

De este modo, el error de Convert.ToInt32 de comienzo del post podríamos solucionar con la siguiente expresión de consulta:

Dim t =
(

  From r In c.Clientes
  Where r.Nombre.StartsWith("C")
).AsEnumerable.Where(
Function(p) Convert.ToInt32(p.IdCliente) > 1)


Un saludo!

2 comentarios:

  1. Esta hecho un cracker. Ganas tengo de ver todas las consultas con LINQ!

    ResponderEliminar
  2. Me has dejado las cosas más claras. Gracias la entrada.

    ResponderEliminar