Operadores de consulta estándar con LINQ

Language Integrated Query (LINQ) permite a los desarrolladores escribir consultas similares a SQL con el código de Microsoft® .NET Framework 3.5 usando una sintaxis con establecimiento inflexible de tipos. Distintos proveedores de LINQ, como LINQ to Objects (que le permite escribir las consultas con jerarquías de objetos) y LINQ to Entities (que le permiten escribir consultas con el modelo conceptual de Entity Framework), podrán procesar estas consultas de manera eficaz en función de las particularidades de sus almacenes de datos representativos.


 


 


Además de la sintaxis con establecimiento inflexible de tipos, las consultas de LINQ tienen también un arsenal de operadores de consulta estándar para mejorar su funcionalidad. Estos operadores de consulta estándar funcionan en secuencias y le permiten realizar operaciones como determinar si un valor existe en la secuencia y realizar una función agregada, como una suma, sobre una secuencia.


En la columna de este mes, se realizarán consultas y operaciones prácticas con LINQ, usando tanto LINQ to Objects como LINQ to Entities. Se realizará una consulta sobre una colección de entidades y me sumergir en un conjunto jerárquico de entidades mediante sus propiedades de navegación. Se mostrará también cómo puede aplicar muchos de los operadores de consulta estándar a matrices y colecciones. Se demostrará cómo se mejoran los operadores de consulta estándar de LINQ mediante expresiones lambda y cómo les permite analizar información concreta a partir de una secuencia. También aprenderán a realizar una lógica compleja sobre una secuencia. Todos los ejemplos de código están disponibles en la descarga de esta columna (consulte msdn.microsoft.com/msdnmag/code08.aspx).


Operadores y LINQ


LINQ es muy eficaz por sí mismo, independientemente de que use LINQ to XML, LINQ to Datasets, LINQ to Entities, LINQ to Objects o cualquier otro proveedor de LINQ que surja. El punto fuerte de LINQ radica en su sintaxis de consulta con establecimiento inflexible de tipos, que puede usarse para operar con cualquiera de estos proveedores. Al combinar LINQ con uno o varios operadores de consulta estándar, el resultado es un conjunto de herramientas aún más eficaz que le proporciona un control granular sobre un conjunto de datos.


Los operadores de consulta estándar se encuentran en el ensamblado System.Core.dll dentro del espacio de nombres System.Linq como métodos de extensión en las clases estáticas Enumerable y Queryable. Pueden usarse en objetos que implementan IEnumerable<T> o IQueryable<T>. Esto les permite operar con varios tipos, desde colecciones y matrices (secuencias) en memoria hasta bases de datos remotas que usan proveedores como LINQ to Entities y LINQ to SQL.


Es fácil determinar qué conjunto de operadores tiene a su disposición para realizar una tarea específica. Cuando desee usar un operador en una consulta de LINQ, puede utilizar un operador de la lista disponible de métodos de extensión en la clase estática Queryable. Cuando desee usar un operador en una secuencia que implementa IEnumerable<T>, puede usar uno de los métodos de extensión encontrados en la clase estática Enumerable. Tenga presente, sin embargo, que no todos los operadores encontrados en la clase Queryable son aplicables al almacén de datos subyacente y que, por tanto, es posible que algunos presenten problemas de compatibilidad durante el tiempo de ejecución.


Tipos de operadores


Hay muchos tipos diferentes de operadores (que pueden encontrarse mediante el explorador de objetos para mirar en las clases Enumerable y Queryable). La Figura A muestra una categorización de los distintos tipos de operadores por orden alfabético. Le ofrece una visión de la funcionalidad que los operadores ofrecen. Voy a demostrar un subconjunto de estos operadores usando tanto LINQ to Objects como LINQ to Entities para mostrar cómo pueden ser beneficiosos en una aplicación real.


FiguraA


Expresiones lambda


Muchos operadores de consulta estándar usan delegados Func para procesar elementos individuales conforme operan sobre una secuencia. Las expresiones lambda pueden funcionar en este caso junto con los operadores de consulta estándar para representar a los delegados. Una expresión lambda es una forma abreviada para crear una implementación de un delegado y puede utilizarse en cualquier lugar donde se pueda usar un delegado anónimo. Las expresiones lambda se admiten tanto en C# como en Visual Basic® .NET. Sin embargo, es importante tener en cuenta que, puesto que Visual Basic .NET no admite aún métodos anónimos, las expresiones lambda pueden constar de una única instrucción.


Vamos a ver cómo se puede usar el operador Single en una matriz de enteros. Se crea una matriz de enteros, donde cada elemento representa una potencia de 2 del 1 al 10. Después se usará el operador Single para recuperar el elemento del entero que reúne la condición especificada en la expresión lambda:


 

int[] nums = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 };
int singleNum = nums.Single(x => x > 16 && x < 64);
Console.WriteLine(singleNum.ToString());

Las expresiones lambda tienen unos cuantos elementos clave. La expresión lambda se inicia con la definición de las variables que se pasan al delegado. En el ejemplo de código anterior, x (declarado a la izquierda del operador =>) es el argumento que representa cada elemento de la matriz numérica que se pasa a él. El resto de la expresión lambda representa la lógica que se evalúa para cada elemento de la matriz. La expresión anterior se podría haber rescrito fácilmente usando un delegado anónimo, como se muestra a continuación:


 

int singleNum = nums.Single<int>(
delegate(int x) {return (x > 16 && x < 64); }
) ;

Sin embargo, este código es menos legible que usar la expresión lambda. C# 2.0 introdujo delegados anónimos, lo que simplificó en gran medida el paso de delegados. Sin embargo, las expresiones lambda simplificaron el proceso exponencialmente gracias a la brevedad de su sintaxis.


First y Single


Cuando es necesario obtener un solo valor de una secuencia, los operadores First, FirstOrDefault, Single y SingleOrDefault son muy útiles. El método First devuelve el primer elemento de una secuencia. Hay un método sobrecargado para First que le permite pasar una expresión lambda para representar una condición. Por ejemplo, si desea devolver el primer elemento de una secuencia de enteros donde el elemento entero es mayor que 50, podría usar el siguiente ejemplo de código:


 

int[] nums = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 };
int num1 = nums.First<int>();
int num2 = nums.First<int>(x => x > 50);
int num3 = nums.FirstOrDefault<int>(x => x > 5000);

Console.WriteLine(
num1.ToString() + «-» +
num2.ToString() + «-» +
num3.ToString());


Este código encontrará el primer elemento (1), el primer elemento mayor que 50 (64) y el primer elemento mayor que 5.000. Debido a que no hay ningún elemento en la matriz que satisfaga la tercera expresión lambda (ningún entero de la matriz es superior a 5.000), se generará una excepción si el código usó el operador First en lugar de FirstOrDefault. Al usar el operador FirstOrDefault, se devuelve 0 si ningún elemento satisface la expresión lambda. También se puede usar el operador First en una consulta de LINQ to Entities, como puede ver a continuación:


 

using (Entities entities = new Entities())
{
var query = (from c in entities.Customers
select c).First(c => c.City.Equals(«London»));
Console.WriteLine(query.CompanyName);
}

En este ejemplo, se devolverá el primer cliente en la ciudad de Londres. Como puede ver, la sintaxis para usar el método First no cambia cuando se usa con distintos proveedores de LINQ (en este caso, LINQ to Objects y LINQ to Entities).


El operador First es muy útil en el contexto de LINQ to Entities, sobre todo cuando sabe que la consulta devolverá un único registro. Por ejemplo, podría tener una consulta que siempre obtiene un registro de cliente cuando se proporciona CustomerID. En esta situación, siempre se devolvería 0 o 1 registro, por lo que una secuencia no ayuda tanto como la entidad en sí misma. En otras palabras, simplemente recogería la entidad Customer en lugar de una secuencia de la entidad 1 Customer. Este es un caso en el que el método First puede ayudar, tal y como se muestra en el fragmento de código que sigue. Debido a que Entity Framework no intenta distribuir la ejecución de una consulta entre cliente y servidor, el método Single no se admite en LINQ to Entities, por lo que usar el método First es una alternativa fácil.


 

using (Entities entities = new Entities())
{
var query = (from c in entities.Customers
where c.CustomerID.Equals(«BOLID»)
select c).First();
Console.WriteLine(query.CompanyName);
}

Agregados, jerarquías y proyecciones


Usar un operador agregado como Sum en una consulta LINQ to Entities puede ayudar a simplificar una consulta. Por ejemplo, el código siguiente recupera una secuencia de pedidos donde la cantidad total es superior a 10.000 USD:


 

using (Entities entities = new Entities())
{
var query = from o in entities.Orders
where o.OrderDetails.Sum(
od => od.UnitPrice * od.Quantity) >= 10000
select o;
foreach (Orders order in query)
Console.WriteLine(order.OrderID);
}

Como LINQ puede realizar una consulta sobre las recopilaciones de entidades jerárquicas, los operadores de consulta estándar también se pueden usar para realizar operaciones sobre las secuencias anidadas de entidades. Esto puede ser muy útil cuando los datos derivados deben calcularse o interrogarse. Los datos derivados sólo pueden existir en sus formularios base. Éste es el caso, por ejemplo, cuando los detalles de los pedidos del cliente sólo tienen valores para el precio por unidad y la cantidad. En este caso, los datos agregados que representan la cantidad total correspondiente a un pedido no se proporcionan en ningún punto del modelo. Sin embargo, al aplicar el operador Sum a su consulta de LINQ, podrá seguir recuperando todos los clientes que hayan gastado más de 20.000 USD, tal y como vemos aquí:


 

using (Entities entities = new Entities())
{
var query = from c in entities.Customers
where c.Orders.Sum(
o => o.OrderDetails.Sum(
od => od.UnitPrice * od.Quantity)) >= 25000
select c;
foreach (Customers customer in query)
Console.WriteLine(customer.CompanyName);
}

Este ejemplo muestra cómo puede aplicar los operadores de consulta estándar en varios niveles de una consulta de LINQ. La consulta devuelve finalmente una secuencia de entidades Customers, pero para llegar hasta allí, deberá sumergirse primero en los pedidos del cliente y en cada detalle del pedido para obtener los datos que necesita. Así, se puede calcular el precio de cada elemento de línea, sumar el total de los elementos de línea correspondiente a cada pedido y sumar los totales del pedido para cada cliente.


El operador Count es otro operador de consulta estándar agregado. Puede determinar cuántos clientes gastaron más de 25.000 USD con el siguiente código:


 

using (Entities entities = new Entities())
{
var query = (from c in entities.Customers
where c.Orders.Sum(
o => o.OrderDetails.Sum(
od => od.UnitPrice * od.Quantity)) >= 25000
select c).Count();
Console.WriteLine(query);
}

Se puede usar el operador Max para determinar el mejor cliente. El siguiente código de ejemplo devolverá la cantidad desembolsada por el cliente que más gasta. Usa una combinación de los agregadores Sum y Max en varios niveles de una jerarquía:


 

using (Entities entities = new Entities())
{
var query = (from c in entities.Customers
select new
{
c.CustomerID,
Total = c.Orders.Sum(
o => o.OrderDetails.Sum(od => od.UnitPrice))
}).Max(c2 => c2.Total);
Console.WriteLine(query);
}

Proyecciones y ordenación


Puede que haya notado que en el ejemplo anterior he introducido una proyección. Antes usar el operador Max, la consulta LINQ no devuelve una lista de clientes. En su lugar, devuelve una proyección, que crea una entidad nueva que contiene una propiedad para CustomerID y otra para Total (el importe total del gasto del cliente). Las proyecciones son una parte integral de LINQ y, cuando se proyectan en secuencias, tal y como se muestra en el ejemplo anterior, se pueden seguir procesando mediante operadores de consulta estándar.


La Figura 1 muestra cómo crear una proyección de una nueva entidad que contiene un CustomerID y la suma de los totales de pedidos del cliente (con el operador Sum que se ha descrito anteriormente). La Figura 1 usa el operador OrderByDescending para ordenar la secuencia de entidades proyectadas en función del total calculado. Si dos clientes tuvieran el mismo total, se podría usar un operador de ordenación adicional para ampliar la definición de la ordenación. Por ejemplo, la instrucción foreach que aparece en la Figura 1 se podría corregir para usar el siguiente código y seguir calificando las reglas de ordenación:


Figura 1 Agregados, proyecciones y ordenación


 

using (Entities entities = new Entities())
{
var query = from c in entities.Customers
where c.Orders.Sum(
o => o.OrderDetails.Sum(od => od.UnitPrice)) > 0
select new
{
c.CustomerID,
Total = c.Orders.Sum(
o => o.OrderDetails.Sum(od => od.UnitPrice))
};
foreach (var item in query.OrderByDescending(x => x.Total))
Console.WriteLine(item.CustomerID + » == » + item.Total);
}


 

foreach (var item in
query.OrderByDescending(x => x.Total)
.ThenBy(x => x.CustomerID))
{
Console.WriteLine(item.CustomerID + » == » + item.Total);
}

En este fragmento de código, agregué el operador ThenBy y una expresión lambda para indicar que la secuencia debe ordenarse primero en orden descendente por la propiedad Total y después en orden ascendente por la propiedad CustomerID de la proyección.


Cuantificadores y conversión


Si necesita determinar si existe un valor en una secuencia, puede usar el operador de consulta estándar Any. Los cuantificadores como Any, All y Contains buscan una secuencia de elementos y evalúan si la secuencia cumple o no una condición de la expresión lambda. Esto puede ser muy útil cuando necesita examinar una secuencia para determinar, por ejemplo, si el cliente de una dirección específica existe, si todos los clientes son del mismo país o cualquier otra pregunta analítica.


Por ejemplo, la siguiente consulta LINQ comprueba si todos los clientes del Reino Unido se encuentran en Londres. Usa el cuantificador All y le pasa la expresión lambda que evalúa si la ciudad es Londres. Si cada elemento en la secuencia cumple este criterio y devuelve true desde la expresión lambda, entonces el operador All devolverá true:


 

using (Entities entities = new Entities())
{
bool allUKCustomerAreFromLondon = (from c in entities.Customers
where c.Country == «UK»
select c).All(
c => c.City.Equals(«London»));
Console.WriteLine(allUKCustomerAreFromLondon ? «Yes» : «No»);
}

Otra pregunta que quizás necesite respuesta en esta consulta es si hay alguna entidad en la secuencia de la ciudad de Cowes en el Reino Unido. Con esta pregunta, puede usar el cuantificador Any para operar en la secuencia, tal y como se muestra aquí:


 

using (Entities entities = new Entities())
{
bool isOneUKCustomerFromCowes = (from c in entities.Customers
where c.Country == «UK»
select c).Any(
c => c.City.Equals(«Cowes»));
Console.WriteLine(isOneUKCustomerFromCowes? «Yes» : «No»);
}

El operador Contains es similar al operador Any en tanto que evalúa si la secuencia contiene lo que está buscando. El operador Any puede determinar si un valor existe dentro de un elemento en la secuencia, pero el operador Contains determina si un elemento concreto existe en la secuencia. Por ejemplo, antes de agregar un objeto a una secuencia, es posible que desee asegurarse de que la secuencia no contiene ese objeto. La Figura 2 muestra cómo hacerlo.


Figure 2 Uso de Contains y conversión


 

using (Entities entities = new Entities())
{
Customers customerBSBEV = (from c in entities.Customers
where c.CustomerID == «BSBEV»
select c).First();

var customersUK = from c in entities.Customers
where c.Country == «UK»
select c;

bool isCustomerInSequence = customersUK.Contains(customerBSBEV);

Console.WriteLine(isCustomerInSequence? «Yes» : «No»);
}


Tenga en cuenta que en la Figura 2 se recupera una entidad Customers para el cliente BSBEV. A continuación, se recupera la secuencia de entidades Customers de clientes del Reino Unido. Finalmente, se usa el operador Contains para comprobar si la secuencia de clientes contiene la versión mantenida por la variable customerBSBEV.


La implementación del operador Contains que se muestra en la Figura 2 funciona en situaciones donde puede comparar de forma segura objetos basados en la versión actual. Sin embargo, ¿qué ocurre si necesita comprobar el operador Contains en función de una identidad lógica? Afortunadamente, el operador Contains cuenta con una sobrecarga que le permite pasar un objeto que implementa la interfaz de IEqualityComparer<T>. Para usar Contains en función de CustomerID, el código de la Figura 2 puede volver a escribirse de la siguiente manera:


 

using (Entities entities = new Entities())
{

bool isCustomerInSequence = customersUK.Contains(customerBSBEV,
new CustomerComparer());

Console.WriteLine(isCustomerInSequence? «Yes» : «No»);
}


donde CustomerComparer se define como


 

private class CustomerComparer : IEqualityComparer<Customers>
{
public bool Equals(Customers x, Customers y) {
if (x == null || y == null)
return false;
return x.CustomerID.Equals(y.CustomerID);
}


}


Conclusión


Hay muchos operadores de consulta estándar que se definen como métodos de extensión para las clases de secuencias Enumerable y Queryable. Estos operadores pueden ayudar a ampliar la funcionalidad de LINQ, tal y como he mostrado en este artículo. He demostrado también cuántas de las nuevas mejoras realizadas a .NET Framework 3.5 (incluidas las expresiones lambda, LINQ, Entity Framework y las variables con establecimiento implícito de tipos) se unen para facilitar la escritura de código y lógica robustos.


Código lo encuentras en:


URL: http://msdn2.microsoft.com/es-mx/magazine/cc337979.aspx?pr=blog

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *