AGRUPACIONES (GROUP BY)
Tras saber cómo filtrar
elementos, es buen momento para aprender a agruparlos. Agrupar elementos, como
su propio nombre indica, es concentrar los datos de un registro a partir de una
característica común. Por ejemplo, saber los pedidos que se corresponden a cada
uno de los clientes.
Su sintaxis es la
siguiente:
1 2 3 |
var agrupacion = from p in DataLists.ListaPedidos group p by p.IdCliente into grupo select grupo; |
Lo cual nos devolverá un
listado de agrupaciones (objeto que implementa la interfaz
IGrouping<tipoClave, tipoObjetoAgrupado>) compuesto por dos elementos
principales:
§ Key: contiene la clave de la agrupación, es decir, el campo por el cual
se está agrupando. En este caso se trataría del valor de p.IdCliente.
§ <Implícito>: el objeto en sí también es un listado compuesto por
los objetos sobre los que itera la cláusula from, es decir, contenidos
en DataList.ListaPedidos. En este caso sería un listado de objetos de
tipo Pedido. Dado que están agrupados, el objeto grupo sólo
contendrá aquellos objetos de la clase Pedido cuyo valor Pedido.IdCliente sea
el mismo en todos los elementos de la lista.
Así, podríamos anidar
perfectamente dos bucles foreach para recorrer con el primero la
lista de claves por las que se agrupa (grupo.Key) y utilizar el bucle interno
para recorrer la lista de objetos que se ajustan al criterio de agrupación:
1 2 3 4 5 6 |
foreach (var grupo in agrupacion) { Console.WriteLine("ID
Cliente: " + grupo.Key); foreach (var objetoAgrupado in grupo) Console.Write("\t\tPedido
nº " + objetoAgrupado.Id + ":
" + objetoAgrupado.FechaPedido +
"]" + Environment.NewLine); } |
El resultado, el siguiente:
Agrupar por más de un campo
Por supuesto, esto puede
tener utilidades como la de contabilizar el número de pedidos que ha realizado
un cliente. Hagamos una pequeña combinación de join y groupby para
obtener esta información:
1 2 3 4 5 6 7 8 9 10 11 |
var agrupacion = from p in DataLists.ListaPedidos join c in DataLists.ListaClientes on p.IdCliente
equals c.Id group p by new { p.IdCliente, c.Nombre } into grupo select grupo; foreach (var grupo in agrupacion) { Console.WriteLine("Nombre
Cliente: " + grupo.Key.Nombre + " (ID:
" + grupo.Key.IdCliente +
")"); foreach (var objetoAgrupado in grupo) Console.Write("\tPedido
nº " + objetoAgrupado.Id + ":
" + objetoAgrupado.FechaPedido +
"]" + Environment.NewLine); } |
Como podemos observar, la
clave no tiene por qué ser un valor entero. De hecho, no tiene ni siquiera por
qué ser un único elemento: en este caso realizamos una agrupación por ID
de cliente y por nombre, pudiendo acceder a esta información dentro de la
propia clave.
Debemos intentar
«proyectar» nuestra forma de pensar en SQL hacia LINQ. Recordemos que tanto un
lenguaje como otro se basan en aritmética relacional, por lo que lo que
sea válido para un lenguaje, obligatoriamente ha de resultarlo para el otro. Si
sabemos que la siguiente consulta en SQL nos devolverá los pedidos de un
usuario en una base de datos:
1 2 3 4 |
select c.Nombre, p.IdCliente, count(p.Id) from Pedidos p inner join Clientes c on c.Id = p.IdCliente group by p.IdCliente, c.Nombre |
Debemos intentar tener
claro el concepto de que la clave de agrupación son los elementos por los
cuales queremos agrupar. Y así deberá ser también en LINQ. Por lo tanto, la
sentencia SQL
1 |
group by p.IdCliente, c.Nombre |
Se corresponderá en LINQ
con
1 |
group p by new { p.IdCliente, c.Nombre } into grupo |
Como podemos comprobar, la
diferencia es mínima. Y el resultado, como podemos observar, será el siguiente:
Anidar agrupaciones
Gracias a la versatilidad
de LINQ, podemos ir todavía un poco más allá y anidar las
agrupaciones, de modo que obtengamos todas las líneas de pedido que pertenecen
a un pedido y a su vez, todos los pedidos que pertenecen a un cliente.
Aprender este concepto no
es difícil, pero anidar código siempre contribuye a la confusión. Por eso
iremos poco a poco indicando lo que deberíamos hacer en cada caso. Comenzaremos
con la consulta anterior. Ésta consulta nos devolverá un único objeto que
implementa la interfaz IGrouping<TKey, TElement>, siendo:
§ TKey: clave del grupo, compuesto por el tipo anónimo {int, string} (Id
de cliente y nombre de cliente)
§ TElement: tipo de los valores almacenados, en este caso, Pedido.
Por lo tanto, el siguiente fragmento
de código generará un grupo con una clave de dos elementos y una lista de
objetos de la clase Pedido:
1 2 3 4 5 6 7 8 9 10 |
var consultaClientes = from pedido in DataLists.ListaPedidos join cliente in DataLists.ListaClientes on pedido.IdCliente equals cliente.Id group pedido by new { cliente.Id, cliente.Nombre }
into pedidosPorCliente select pedidosPorCliente;
// Key = {Id de cliente, Nombre de cliente} //
Grupo = List |
También queremos obtener
agrupadas todas las líneas de pedido asociadas a un único pedido, agrupando por
el Id del pedido y su fecha de realización. Su estructura será similar a la que
acabamos de generar, salvo que en lugar de usar las entidades Cliente y Pedido usaremos
las entidades Pedido y LineaPedido. De momento, y para
aclararnos, crearemos una nueva consulta LINQ que sea independiente a la
anterior.
1 2 3 4 5 6 7 8 9 |
var consultaPedidos = from lineaPedido in DataLists.ListaLineasPedido join pedido in DataLists.ListaPedidos on lineaPedido.IdPedido equals pedido.Id group lineaPedido by new { pedido.Id, pedido.FechaPedido }
into lineasPorPedido
// Key = {Id de pedido, Fecha de realización del pedido} select lineasPorPedido;
// Grupo = List |
La tercera consulta
implicada ya no precisa de agrupaciones. Se encargará de realizar un join entre Producto y LineaPedido para
poder mostrar en un solo registro la información del producto asociado a la
línea, como su nombre y precio. Además, podremos calcular de forma dinámica el
precio total multiplicando la cantidad de productos indicados en la línea de
producto por el valor unitario de cada producto. Nuevamente, no se trata de una
consulta muy compleja:
1 2 3 4 5 6 7 8 9 10 11 |
var lineaProducto = from linea in DataLists.ListaLineasPedido join producto in DataLists.ListaProductos on linea.IdProducto equals producto.Id select new { IdLineaPedido
= linea.Id, Nombre
= producto.Descripcion, Cantidad
= linea.Cantidad, PrecioUnitario
= producto.Precio, PrecioTotal
= (producto.Precio * linea.Cantidad) }; |
Hasta aquí no
ha habido mucha complicación: hemos realizado tres consultas que se encuentran
conceptualmente relacionadas, pero no hemos establecido una relación entre
ellas. Es hora de unirlas en una única consulta. ¿Cómo realizamos esto?
Recordemos que lo que realiza select es la sentencia encargada de
efectuar las proyecciones. Por lo tanto, si en lugar de indicarle que nos devuelva
un objeto de la serie que estamos recorriendo le decimos que nos devuelva otra
cosa, creará lo que le digamos. Y esa «cosa» puede estar relacionada con la
colección recorrida… y también ser una nueva consulta.
Por tanto,
queda bastante claro que es en a continuación de select donde debemos
conectar nuestras consultas. Comenzaremos uniendo las dos primeras, haciendo
que en global, la sentencia nos devuelva una agrupación de agrupaciones de
líneas de pedido en lugar de que nos devuelva una simple agrupación de pedidos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var consulta = from pedido in DataLists.ListaPedidos join cliente in DataLists.ListaClientes on pedido.IdCliente equals cliente.Id group pedido by new { cliente.Id, cliente.Nombre }
into pedidosPorCliente select from lineaPedido in DataLists.ListaLineasPedido join pedido in DataLists.ListaPedidos on lineaPedido.IdPedido equals pedido.Id group lineaPedido by new { pedido.Id, pedido.FechaPedido }
into lineasPorPedido select lineasPorPedido; |
Como podemos
observar, hemos conectado vilmente el contenido de la segunda consulta y lo
hemos concatenado a continuación de la primera. Sin embargo, lo que nos
devolverá esta consulta será algo con la siguiente estructura:
§
IGroupable<{int, string}, IGroupable<{int,
DateTime}, LineaPedido>
Es decir, un
grupo con clave (int, string) que contiene una lista de grupos (int, DateTime)
que contiene una lista de objetos de la clase LineaPedido. Un poco lioso,
¿verdad? Tranquilos, lo arreglaremos cambiando el valor que devuelve la
primera select. En lugar de que devuelva un grupo en bruto, ¿qué tal si
hacemos que devuelva un tipo anónimo que sea un poco más legible? Por ejemplo,
haremos que el tipo anónimo contenga el ID del cliente, el nombre del cliente
y, ya al final, el listado de agrupaciones, al que le asignaremos un nombre
para poder dirigirnos a él como es debido. Por lo tanto, el fragmento anterior
quedará transformado de la siguiente forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var consulta = from pedido in DataLists.ListaPedidos join cliente in DataLists.ListaClientes on pedido.IdCliente equals cliente.Id group pedido by new { cliente.Id, cliente.Nombre }
into pedidosPorCliente select new { IdCliente
= pedidosPorCliente.Key.Id,
// Id del cliente NombreCliente
= pedidosPorCliente.Key.Nombre,
// Nombre del cliente ListaPedidos
= from lineaPedido in DataLists.ListaLineasPedido //
Grupo de líneas de pedido join pedido in DataLists.ListaPedidos on lineaPedido.IdPedido equals pedido.Id group lineaPedido by new { pedido.Id, pedido.FechaPedido }
into lineasPorPedido select lineasPorPedido }; |
La forma de
esta estructura ya es mucho más sencilla de manejar. Hacemos lo propio con la
segunda select, a la que añadiremos también el ID del pedido y su fecha, que
forman parte de la clave de la agrupación.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var consulta = from pedido in DataLists.ListaPedidos join cliente in DataLists.ListaClientes on pedido.IdCliente equals cliente.Id group pedido by new { cliente.Id, cliente.Nombre }
into pedidosPorCliente select new { IdCliente
= pedidosPorCliente.Key.Id,
// Id del cliente NombreCliente
=
pedidosPorCliente.Key.Nombre,
// Nombre del cliente ListaPedidos
= from lineaPedido in DataLists.ListaLineasPedido //
Grupo de líneas de pedido join pedido in DataLists.ListaPedidos on lineaPedido.IdPedido equals pedido.Id group lineaPedido by new { pedido.Id, pedido.FechaPedido }
into lineasPorPedido select new { IdPedido
=
lineasPorPedido.Key.Id,
// Id del pedido FechaPedido
= lineasPorPedido.Key.FechaPedido, // Fecha del pedido ListaLineas
=
lineasPorPedido
// Listado de objetos de la clase LineaPedido } }; |
Ahora mismo
tendríamos la siguiente estructura:
- Grupo
§ TKey: {int, string}
§ TElement: <tipo anónimo>
- IdCliente (int)
- NombreCliente (string)
- Grupo
§ TKey: {int, fecha}
§ TElement: <tipo anónimo>
§ IdPedido (int)
§ FechaPedido (DateTime)
§ ListaLineas (IEnumerable<LineaPedido>)
Por lo tanto,
para extraer la información del producto, lo único que tendremos que hacer será
sustituir el valor de ListaLineas por la tercera consulta individual
que declaramos más arriba, en la que realizábamos un join entre LineaPedido y Producto y
devolvíamos como resultado un nuevo tipo anónimo con los valores deseados.
Buscamos, por lo tanto, la siguiente estructura:
- Grupo
§ TKey: {int, string}
§ TElement: <tipo anónimo>
- IdCliente (int)
- NombreCliente (string)
- Grupo
§ TKey: {int, fecha}
§ TElement: <tipo anónimo>
§ IdPedido (int)
§ FechaPedido (DateTime)
§ ListaLineas <tipo anónimo>
§ IdLineaPedido (int)
§ Nombre (string)
§ Cantidad (int)
§ PrecioUnitario (float)
§ PrecioTotal (float)
El código final
para nuestra consulta será el siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
var consulta = from pedido in DataLists.ListaPedidos join cliente in DataLists.ListaClientes on pedido.IdCliente equals cliente.Id group pedido by new { cliente.Id, cliente.Nombre }
into pedidosPorCliente select new { IdCliente
= pedidosPorCliente.Key.Id, NombreCliente
= pedidosPorCliente.Key.Nombre, ListaPedidos
= from lineaPedido in DataLists.ListaLineasPedido join pedido in DataLists.ListaPedidos on lineaPedido.IdPedido equals pedido.Id group lineaPedido by new { pedido.Id, pedido.FechaPedido }
into lineasPorPedido select new { IdPedido
= lineasPorPedido.Key.Id, FechaPedido
= lineasPorPedido.Key.FechaPedido, ListaLineas
= from linea in lineasPorPedido join producto in DataLists.ListaProductos on linea.IdProducto equals producto.Id select new { IdLineaPedido
= linea.Id, Nombre
= producto.Descripcion, Cantidad
= linea.Cantidad, PrecioUnitario
= producto.Precio, PrecioTotal
= (producto.Precio * linea.Cantidad) } } }; |
Ahora podremos
iterar tranquilamente para extraer la información de nuestro objeto. El
siguiente código se encargará de ello:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
foreach (var cliente in consulta) { Console.WriteLine(string.Format("CLIENTE:
{0}. Ha
realizado {1} Pedidos", cliente.NombreCliente,
cliente.ListaPedidos.Count())); foreach (var pedido in cliente.ListaPedidos) { Console.WriteLine(string.Format("\tPEDIDO
NUMERO {0} ({1}). Lineas de pedido: {2}", pedido.IdPedido,
pedido.FechaPedido, pedido.ListaLineas.Count())); foreach(var lineaPedido in pedido.ListaLineas) { Console.WriteLine(string.Format("\t\tPRODUCTO:
{0}. Precio {1} x {2} uds. =
{3}", lineaPedido.Nombre,
lineaPedido.Cantidad, lineaPedido.PrecioUnitario,
lineaPedido.PrecioTotal)); } } Console.WriteLine("---------------------------------------------------"); } Console.ReadLine(); |
El resultado de
este código lo podemos ver a continuación: