Nueva característica de “Orcas”: Sintaxis de consultas

El mes pasado empezé una serie de post cubriendo alguna de las nuevas características de VB y C# que vendrán con las nuevas versiones de Visual Studio y del .NET Framework. Aquí teneis los links a los primeros tres post de esta serie:

El post de hoy cubre otra nueva característica fundamental: Sintaxis de consultas.

¿Que es la sintaxis de consultas?

La sintaxis de consultas (en inglés, Query sintax) es un convenio de declaraciones para expresar consultas usando los operadores estándares de LINQ. Aporta una sintaxis que aumenta la claridad a la hora de escribir consultas en el código, y puede ser más facil de leer y escribir correctamente. Visual Studio provee un intellisense y chequeo en tiempo de compilación para la sintaxis de consultas.

Ejemplo de sintaxis de consultas:

En los post anteriores de esta serie, enseñé cómo declarar una clase "Person" de la siguiente forma:

Podemos usar el código de abajo para instanciar una colleción List<Person> con valores de gente, y usar la sintaxis de consultas para construir una consulta de LINQ sobre la colección y obtener aquellas personas cuyo apellido empieze por la letra "G", ordenados por el nombre (ascendentemente):

La expresión anterior es semánticamente equivalente al código de abajo, donde usamos métodos de extensión de LINQ y expresiones lambda explicitamente:

Los beneficios de usar la sintaxis de expresiones es que acaba siendo más facil de leer y escribir. Esto es especialmente cierto ya que la expresión se vuelve más rica y es más descriptiva.

Sintaxis de consultas - Entendiendo las frases From y Select:

Toda expresión sintáctica en C# empieza con un "form" y termina con "select" o "group". La palabra "form" nos indica qué datos queremos consultar. La palabra "select" nos indica qué datos queremos devolver, y qué condiciones deben cumplir.

Por ejemplo, veamos otra vez la consulta sobre la colección List<Person>:

En el código de arriba el trozo "from p in people" nos está indicando que queremos lanzar una consulta LINQ sobre la colección "people", y que usaremos el parámetro "p" para representar a cada elemento de entrada que estamos consultando. En realidad, el nombre del parámetro "p" es irrelevante - podría haberlo llamado "o", "x", "person" o de cualquier otra forma.

El trozo "select p" nos indica que queremos devolver una secuencia IEnumerable de objetos Person como resultado de la consulta. Esto es así ya que la colección "people" contiene objetos de tipo "Person", y el parámetro "p" representa a objetos Person de esa colección. El tipo de dato devuelto por esta expresión es del tipo IEnumerable<Person>.

Si en lugar de devolver objetos Person, quisiéramos devolver sólo el nombre de la colleción de gente, podríamos reescribir la consulta de la siguiente forma:

Fijaos que en el código anterior no estoy diciendo "select p", sino que estamos diciendo "select p.FirstName". Esto indica que no quiero devolver una secuencia de objetos Person - sino una secuencia de string - calculados a partir de la propiedad FirstName de los objetos Person. Por tanto, el tipo devuelto por esta expresión es del tipo IEnumerable<string>

Ejemplo de sintaxis de consultas sobre una base de datos.

La belleza de LINQ es que podemos usar la misma sintaxis sobre cualquier tipo de datos. Por ejemplo, podemos usar el nuevo mapper relacional de LINQ to SQL que viene con "Orcas" para modelar la base de datos de ejemplo de SQL "Northwindw" con clases de la siguiente forma (por favor, ved mi video para aprender cómo hacer esto.)

Una vez que hayamos definido el modelo de clases anterior (y su mapeado a/desde la base de datos), podemos escribir una expresión sintáctica para ver todos los productos cuyo precio por unidad sea mayor de 99 $.

En el código de arriba estamos indicando que queremos lanzar una consulta LINQ sobre la tabla "Products" en la clase NorthwindDataContext creada por el diseñador ORM de Visual Studio "Orcas". El "Select p" indica que queremos devolver una secuencia de objetos Product que cumplan la condición. El tipo de dato que devuelve esta expresión es del tipo IEnumerable<Product>

Como en el ejemplo anterior de List<Person>, el compilador de C# traducirá nuestra consulta en un método de extensión (usando expresiones Lambda como argumentos). En el caso del código anterior con el ejemplo de LINQ to SQL, las expresiones lambda se convertirán en comandos SQL y se evaluarán en el servidor SQL (de forma que sólo las tuplas de la tabla Products son devueltas a nuestra aplicación). Los detalles del mecanismo que permite esta conversión lambda->SQL los podéis encontrar en mi post sobre expresiones lambda en la sección sobre árboles de expresiones lambda.

Sintaxis de consultas - Entendiendo las frases Where y OrderBy

Entre las sentencias "form" y "select" podemos usar la mayoría de los operadores de LINQ para filtrar y transformar los datos que estamos consultando. Dos de las sentencias más comunes que usareis serán "where" y "orderby". Éstas manejan el filtrado y el ordenado de los resultados.

Por ejemplo, para devolver una lista ordenada alfabéticamente por el nombre de las categorías de la base de datos Northwind - filtrando sólo aquellas categorías que tienen más de cinco productos asociados - podríamos escribir la siguiente expresión que usa LINQ to SQL para consultar nuestra base de datos:

En la expresión anterior hemos añadido la sentencia "where c.Products.Count >5" para indicar que sólo queremos devolver los nombres de categorías que tengan asociados más de cinco productos. Esto usa las ventajas ofrecidas por el mapeado hecho por  ORM de LINQ to SQL entre los productos y categorías de nuestra base de datos. Además hemos añadido la sentencia "orderby c.CategoryName descending" para indicar que lo queremos ordenar en orden descendiente.

LINQ to SQL generará la siguiente SQL cuando consulte la base de datos usando nuestra expresión:

SELECT [t0].[CategoryName] FROM [dbo].[Categories] AS [t0]
WHERE ((
    SELECT COUNT(*)
    FROM [dbo].[Products] AS [t1]
    WHERE [t1].[CategoryID] = [t0].[CategoryID]
)) > 5
ORDER BY [t0].[CategoryName] DESC

Fijáos en lo listo que es LINQ to SQL ya que sólo nos devuelve la columna que necesitamos (la CategoryName). También hace todo el filtrado y ordenado en la base de datos - lo que lo hace muy eficiente.

Sintaxis de consultas - Transformando datos con projecciones.

Uno de los puntos que hicimos antes era que la sentencia "select" indica qué datos queremos devolver, y en qué forma debería estar.

Por ejemplo, se tenemos una sentencia "select p" como la anterior - donde p es de tipo Person - debería devolver una secuencia de objetos tipo Person:

Una de las capacidades realmente poderosas de LINQ y la sintaxis de consultas es la abilidad de dejarnos definir nuevas clases que están separadas de los datos que están siendo consultados, y entonces usarlas para controlar la forma y la estructura de datos que son devueltos por la consulta.

Por ejemplo, imaginemos que definimos una nueva clase "AlternatePerson" que tiene una sola propiedad "FullName" en lugar de dos separadas "FirstName" y "LastName" como en la clase "Person" que teníamos:

Podríamos usar la siguiente expresión de LINQ para conslutar la colección de List<Person> original, y transformar los resultados para que sean objetos de la clase AlternatePerson:

Fijáos en cómo podemos usar la sintaxis en la inicialización de objetos de la que hablé en el primer post de esta serie para crear una nueva instancia de AlternatePerson y poner sus propiedades dentro de la sentencia "select". Fijáos también cómo estamos asignando a la propiedad "FullName" concatenando las propiedades "FirstName" y "LastName" de nuestra clase original Person.

Usando projecciones sintácticas con una base de datos.

Esta característica de proyección es increíblemente útil cuando trabajamos con datos obtenidos de un proveedor remoto de datos como una base de datos, nos permite indicar de forma elegante qué columnas debe comprobar nuestro ORM de la base de datos.

Por ejemplo, supongamos que usamos el proveedor del ORM LINQ to SQL para modelar la base de datos "Northwind" en clases:

Con la consulta LINQ de abajo, le estamos diciendo a LINQ to SQL que queremos una secuencia de objetos "Product":

De todas las columnas necesarias para generar la clase Product deberían ser devueltas por la base de datos como parte de la consulta de arriba, y la SQL original ejecutada por el ORM LINQ to SQL sería algo como lo de abajo:

SELECT [t0].[ProductID], [t0].[ProductName], [t0].[SupplierID], [t0].[CategoryID],
              [t0].[QuantityPerUnit], [t0].[UnitPrice], [t0].[UnitsInStock],
              [t0].[UnitsOnOrder], [t0].[ReorderLevel], [t0].[Discontinued]
FROM [dbo].[Products] AS [t0]
WHERE [t0].[UnitPrice] > 99

Si no necesitamos/queremos todas las columnas en algunos escenarios, podríamos definir una nueva clase "MyProduct" como la de abajo, que tenga un subconjunto de propiedades de las de "Product", así como una nueva propiedad - "TotalRevenue"- que la clase Product no tiene (nota: para los que no estén familiarizados con C#, el Decimal? indic que la propiedad UnitPrice es un valor nullable:

Podríamos usar la capacidad de proyección para "encajar" los datos que quiero devolver de la base de datos usando una consulta como la siguiente:

Esto indica que en lugar de devolver una secuencia de objetos "Product", lo que queremos son objetos "MyProduct", y que sólo necesitamos tres propiedades para llenarlos. LINQ to SQL es lo suficientemente listo para ajustar la SQL original para devolver aquellas tres columnas que necesitamos de la base de datos:

SELECT [t0].[ProductID], [t0].[ProductName], [t0].[UnitPrice]
FROM [dbo].[Products] AS [t0]
WHERE [t0].[UnitPrice] > 99

Sólo por fanfarronear, podríamos calcular la cuarta propiedad de la clase MyProduct - "TotalRevenue". Queremos que este valor sea la cantidad de unidades que hemos vendido de ese producto. Este valor no se guarda en ninguna parte como una columna precalculada en la base de datos Northwind. Tendríamos que hacer un join entre las tablas "Products" y "Order Details" y sumar todas las tuplas de Order Detail asociadas al producto dado.

Lo chulo es que podemos usar el método de extensión de LINQ "Sum" para aprovechar la asociación de Products con OrderDetails y escribir una expresión lambda para la multiplicación como parte de mi sintaxis de consulta para calcular este valor.

LINQ to SQL es lo suficientemente listo para usar la siguiente SQL para hacer el cálculo en la base de datos:

SELECT [t0].[ProductID], [t0].[ProductName], [t0].[UnitPrice], (
        SELECT SUM([t2].[value])
        FROM (
                 SELECT [t1].[UnitPrice] * (CONVERT(Decimal(29,4),[t1].[Quantity])) AS [value], [t1].[ProductID]
                 FROM [dbo].[Order Details] AS [t1]
                 ) AS [t2]
        WHERE [t2].[ProductID] = [t0].[ProductID]
        ) AS [value]
FROM [dbo].[Products] AS [t0]
WHERE [t0].[UnitPrice] > 99

Sintaxis de consultas - Entendiendo la ejecución retrasada, y el uso de ToList() y ToArray()

Por defecto, el resultado de una expresión sintáctica es una variable del tipo IEnumerable<T>. En los ejemplos de arriba os habréis dado cuenta que todas las expresiones devuelven IEnumerable<Product>, IEnumerable<string>, IEnumerable<Person>, IEnumerable<AlternatePerson> e IEnumerable<MyProduct>.

Una de las principales características de la interface IEnumerable<T> es que los objetos que las implementen pueden retrasar su ejecución hasta el momento en que el desarrollador intente iterar sobre los valores (esto es posible gracias al constructor "yield" que fue introducido en C#2.0 en VS 2005). LINQ y las expresiones sintácticas usan esta característica, y retrasan la ejecución de las consultas hasta la primera vez que se itere sobre los resultados. Si nunca se itera sobre una IEnumerable<T>, entonces, la consulta nunca es ejecutada.

Por ejemplo, consideremos el siguiente ejemplo LINQ to SQL:

La base de datos será avisada y los valores necesarios para calcular nuestros objetos Category serán obtenidos no cuando se declara la expresión - sino la primera vez que intentemos iterar sobre los resultados (en la flecha roja).

Esta ejecución retrasada es muy útil porque nos permite crear escenarios muy potentes donde podamos encadenar multiples consultas LINQ y expresiones juntas. Por ejemplo, podríamos usar el resultado de una expresión dentro de otra - y retrasando la ejecución permitimos a un ORM como LINQ to SQL optimizar toda la sentencia SQL que se lanzará en el árbol de expresiones. Veamos unos ejemplos de cómo usar esto en otro post más adelante.

Cómo evaluar una expresión imediatamente.

Si no queremos retrasar la ejecución de las consultas, y queremos hacerlo inmediatamente, podemos usar los operadores ToList() y ToArray() para devolver una List<T> o un array con los resultados.

Por ejemplo, para devolver una lista genérica List<T>:

y para devolver un array:

En ambos casos la base de datos será consultada inmediatamente.

Resumen:

La sintaxis de consultas nos permiten obtener ciertas características declarativas para expresar consultas usando los operadores standar de LINQ. Nos ofrece una sintaxis muy fácil de entender y que funciona con casi cualquier tipo de datos (cualquier colección en memoria, arrays, XML, o sobre proveedores de datos remotos como bases de datos, servicios web, etc). Una vez que se nos haga familiar esta sintaxis, podemos aplicarla inmediatamente en cualquier lugar.

En un futuro no muy lejano terminaré con al última parte de esta serie de post - que cubrirá la nueva característica de tipos anónimos. Y luego veremos cómo usar todas estas características en el mundo real (especialmente usando LINQ contra bases de datos y archivos XML).

Espro que os haya servido.

Scott.

Traducido por: Juan María Laó Ramos. Microsoft Student Partner.

5 pensamientos en “Nueva característica de “Orcas”: Sintaxis de consultas

  1. Pingback: Nueva característica de "Orcas": Tipos anónimos « Thinking in .NET

  2. Pingback: Usando LINQ to XML (y como crear un lector de RSS con él) « Thinking in .NET

  3. electrocucaracha

    La verdad no me siento muy comodo dejando al compilador realizar las consultas, considero que las consultas hacia la base de datos no son optimizadas y puede afectar el performance de la base de datos, siempre supuse que una buena practica era siempre crear procedimientos almacenados, ya que estos son precompilados evitando muchos pasos que no son evitados por la ejecucion de consultas independientes. Ademas existen distintos caminos para obtener los mismos datos, pero con diferentes consultas permitiendonos asi escoger la mas optima.

    Responder
  4. Vio

    Electrocucaracha: Entiendo lo que dices. También debes entender que las cosas se pueden hacer de muchas formas. El uso de los procedimientos almacenados es muy ventajosa, yo la recomiendo. Pero hay que tener en cuenta que la mayoría de las consultas que se lanzan a la base de datos van a ser, la mayoría de las veces, simples. De hecho, si te riges por el principio (KIS, Keep It Simple) esas consultas parecerán muy tontas, pero vistas desde la distancia, la suma de todas ellas consiguen una solución más “bonita”, entendible y hasta más eficiente que si usas una sentencia más larga, compleja y poco entendible.
    Date cuenta también que desde que aparecieron los primeros compiladores lo que se está haciendo es traducir de un lenguaje de alto nivel a lenguaje máquina. Se ha avanzado mucho en este terreno, y todo ese Know How se ha llevado a LINQ.
    No digo que todas las consultas que genere LINQ to SQL (por concretar) sean siempre las más eficientes, pero si la mayoría. Para comprobarlo no tienes más que activar el SQL Profiler de Sql server y ver qué consultas se están ejecutando realmente. Sin embargo, para consultas más pesadas, más artesanas, que rompen con el principio KIS, ahí tienes toda la potencia y libertad de escribir un procedimiento almacenado que te lo haga y seguir usando LINQ para las demás. En mi humilde opinión, como tu comentas al final, si necesito una consulta de este tipo, siempre me planteo cambiarla, no siempre lo consiguo, pero la mayoría de las veces (por no decir todas) encuentro una solución más óptima.

    Espero que sirva.

    Responder

Deja un comentario