Pese a que LINQ to Entities es bastante parecido a LINQ to SQL, a la hora de trabajar con ambas tecnologías es necesario conocer las diferencias más importantes a nivel práctico (dejaremos los fundamentos teóricos a un lado). Ambas tecnologías tienden a la convergencia, puesto que Microsoft está intentando coger lo mejor de cada una de ellas y adaptarlo a ambos mundos. Sin embargo, siguen existiendo diferencias importantes, especialmente en las primeras versiones de LINQ to Entities.
Select. Carga diferida explícita.
Las primeras versiones de Entity Framework contenían ciertas carencias que, con posteriores actualizaciones, han sido solventadas y corregidas. Una de las carencias más importantes se correspondería con la imposibilidad de las versiones anteriores a 4.1 de realizar una carga automática de los elementos referenciados por un objeto. Es decir, si tenemos el siguiente código:
// Instanciamos el contexto var contexto = new testdbEntities(); // Lanzamos una consulta var clientes = from cliente in contexto.Clientes select cliente; // Recorremos los clientes foreach (Cliente cliente in clientes) { Console.WriteLine(string.Format("ID: {0}\tNOMBRE: {1}\tAÑO NAC: {2}", cliente.IdCliente, cliente.Nombre, cliente.FechaNacimiento.Year)); // Recorremos los pedidos foreach (Pedido pedido in cliente.Pedidos) { Console.WriteLine(string.Format("\tPEDIDO: {0}\tFECHA: {1}", pedido.IdPedido, pedido.FechaPedido)); // Recorremos las líneas de pedido foreach (LineaPedido linea in pedido.LineasPedido) { Console.WriteLine(string.Format("\t\tPRODUCTO: {0}\tCANTIDAD: {1}\tTOTAL: {2}", linea.Producto.Descripcion, linea.Cantidad, (linea.Producto.Precio*linea.Cantidad))); } } Console.WriteLine(" -----------------------------------------\n"); }
Las versiones anteriores a la 4.1 eran incapaces de iterar sobre los pedidos de un cliente, ya que era necesario realizar una carga implícita de estos elementos antes de utilizarlos. Esto se hacía mediante el método Load(), que debía ser invocado antes de hacer uso del listado:
foreach (Cliente cliente in clientes) { Console.WriteLine(string.Format("ID: {0}\tNOMBRE: {1}\tAÑO NAC: {2}", cliente.IdCliente, cliente.Nombre, cliente.FechaNacimiento.Year)); cliente.Pedidos.Load(); // Recorremos los pedidos foreach (Pedido pedido in cliente.Pedidos) { // ...
Otro modo de realizar lo mismo sería mediante la cláusula Include al realizar la consulta, especificando mediante una cadena de texto el nombre de los elementos de la relación que se quieren incluir:
var clientes = contexto.Clientes.Include("Pedidos");
Afortunadamente, la carga diferida automática propia de LINQ to SQL ha sido incluida en Entity Framework 4.1, y ya no es necesario realizar esta invocación en las versiones actuales. Sin embargo, si estás trabajando con versiones anteriores, es necesario conocer esta carencia.
Insert
La inserción también variará dependiendo de nuestra versión del Entity Framework. A partir de 4.1 podremos realizar inserciones mediante el método Add del listado correspondiente contenido dentro del DbContext. En versiones anteriores, deberemos usar los métodos AddObject(), que añadirá cualquier entidad siempre y cuando le pasemos el nombre de ésta) o AddClientes() (se generará un método para cada entidad dentro de nuestro ObjectContext).
Una vez realizadas las inserciones, se invocará el método SaveChanges() para comprometer los cambios (recordemos que el método equivalente en LINQ to SQL era SubmitChanges()).
// Instanciamos el DbContext var dbContext = new testdbEntities(); // Extraemos el ObjectContext del DbContext (a partir de Entity Framework 4.1) var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; Cliente katiaRamos = new Cliente() { Nombre = "Katia Ramos Oliveira", FechaNacimiento = new DateTime(1991, 12, 4) }; Cliente arturoSaavedra = new Cliente() { Nombre = "Arturo Saavedra Gonzalez", FechaNacimiento = new DateTime(1987, 4, 14) }; Cliente guillermoSanabria = new Cliente() { Nombre = "Guillermo Sanabria San Juan", FechaNacimiento = new DateTime(1983, 11, 1) }; // MÉTODO 1: Método Add – Versiones 4.1 y superiores dbContext.Clientes.Add(katiaRamos); // MÉTODO 2: AddObject (genérico) objectContext.AddObject("Clientes", arturoSaavedra); // MÉTODO 3: AddClientes (específico del contexto) – Versiones anteriores a la 4.1 objectContext.AddClientes(guillermoSanabria); // Guardamos los cambios dbContext.SaveChanges(); // Comprobamos si todo es correcto foreach (Cliente cliente in dbContext.Clientes) { Console.WriteLine(string.Format("ID: {0}\tNOMBRE: {1}\tAÑO NAC: {2}", cliente.IdCliente, cliente.Nombre, cliente.FechaNacimiento.Year)); }
Hecho esto, comprobamos que todo sea correcto:
Si lo que queremos es insertar elementos asociados, Entity Framework se encargará de realizar las inserciones asociadas si los elementos no existían previamente. Así, si le asociamos un pedido al cliente 9 con una línea de pedido asociada a un producto, bastaría con añadir la línea de pedido si ésta ha incluido previamente la referencia al pedido (aunque aún no esté insertado en la base de datos).
// Realizamos la consulta Cliente cliente = dbContext.Clientes.Where(c => c.IdCliente == 9).First(); // Creamos los registros asociados: Pedido p = new Pedido() { Cliente = cliente, FechaPedido = DateTime.Now, }; LineaPedido lp = new LineaPedido() { Pedido = p, Producto = dbContext.Productos.Where(prod => prod.IdProducto == 4).First(), Cantidad = 7 }; // Insertamos registros la línea de pedido, que ya tiene asociado el pedido. // LINQ to Entities se encargará de realizar la inserción previa del pedido por nosotros. dbContext.LineasPedido.Add(lp); // Guardamos los cambios dbContext.SaveChanges();
Update
El proceso de actualización sera similar al que vimos en LINQ to SQL: se realizará la modificación sobre el objeto y se guardarán los cambios mediante el método SaveChanges().
// Instanciamos el DbContext var dbContext = new testdbEntities(); // Realizamos la consulta var clientes = dbContext.Clientes.Where(cliente => cliente.Nombre.StartsWith("Katia")); // Modificamos los objetos que consideremos oportunos foreach (var cliente in clientes) cliente.Nombre = cliente.Nombre.Replace("Katia", "Katerina"); // Guardamos los cambios dbContext.SaveChanges(); // Comprobamos si todo es correcto foreach (Cliente cliente in dbContext.Clientes) { Console.WriteLine(string.Format("ID: {0}\tNOMBRE: {1}\tAÑO NAC: {2}", cliente.IdCliente, cliente.Nombre, cliente.FechaNacimiento.Year)); }
El resultado:
Delete
El proceso de borrado será el inverso al de inserción, mediante la utilización del método Remove():
// Instanciamos el DbContext var dbContext = new testdbEntities(); // Extraemos el ObjectContext del DbContext (a partir de Entity Framework 4.1) var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; // Realizamos la consulta var clienteEliminar = dbContext.Clientes.Where(cliente => cliente.IdCliente == 8).First(); // Eliminamos el cliente objectContext.DeleteObject(clienteEliminar); // Para el Framework 4.0 o inferior dbContext.Clientes.Remove(clienteEliminar); // Para el Framework 4.1 o superior // Guardamos los cambios dbContext.SaveChanges(); // Comprobamos si todo es correcto foreach (Cliente cliente in dbContext.Clientes) { Console.WriteLine(string.Format("ID: {0}\tNOMBRE: {1}\tAÑO NAC: {2}", cliente.IdCliente, cliente.Nombre, cliente.FechaNacimiento.Year)); }
Comprobamos si el cliente 8 ha sido eliminado. Como podemos observar, así ha sido.
Sin embargo, ¿qué ocurrirá si eliminamos un cliente que tenga asociado algún pedido, como por ejemplo el cliente 9? Veamos:
Según observamos, no es posible borrar el cliente si no eliminamos antes sus objetos asociados. Sin embargo, tal y como vimos en LINQ to SQL, no podemos hacer uso del método Remove() para realizar eliminaciones en cascada, ya que este método se limita a eliminar las referencias entre los distintos objetos, no los objetos en sí. Por tanto, ¿cuál será la solución?
Por desgracia, en este caso deberemos limitarnos a decir «así no lo hagas«. Entity Framework no es capaz de manejar bien los borrados en cascada, por lo que deberemos delegar las operaciones de borrado a la base de datos, bien a través de una restricción de borrado en cascada, bien mediante la creación de un procedimiento almacenado que se encargue de realizar este proceso (que podremos invocar también desde nuestro DbContext.
Un ejemplo para esto sería crear dos procedimientos almacenados: uno que borre los pedidos y sus líneas de pedido y otro que, además de borrar pedidos y líneas, borre también los clientes. El primero de ellos será algo como lo siguiente:
create procedure sp_Pedido_DeleteCascade @IdPedido int as begin set nocount on; delete from LineaPedido where IdPedido = @IdPedido; delete from Pedido where IdPedido = @IdPedido; end;
El segundo procedimiento utilizará un cursor que recorrerá los identificadores de los pedidos e invocará por cada uno el procedimiento anterior, borrando, además de los clientes, sus pedidos y líneas de pedido asociadas.
create procedure sp_Cliente_DeleteCascade @IdCliente int as begin set nocount on; -- Declaramos un cursor que obtenga todos los Ids de los pedidos asociados -- al cliente que queremos borrar declare cur cursor for select distinct IdPedido from Pedido where IdCliente = @IdCliente; declare @IdPedido int; -- Borramos todos los pedidos junto a sus lineas de pedido, recorriendo los -- pedidos devueltos por el cursor. open cur; fetch next from cur into @IdPedido while @@FETCH_STATUS = 0 begin exec sp_Pedido_DeleteCascade @IdPedido; fetch next from cursorPedidos into @IdPedido end; close cur; delete from Cliente where IdCliente = @IdCliente; end;
A continuación iremos a nuestro Model Browser e importaremos los dos procedimientos almacenados que acabamos de crear. Haremos click derecho sobre nuestro modelo y seleccionaremos Update Model from Database…
Seleccionaremos los procedimientos almacenados que acabamos de crear (o como mínimo, sp_Cliente_DeleteCascade, que es el que vamos a utilizar)
Codificamos lo siguiente en nuestro método de borrado:
// Instanciamos el DbContext var dbContext = new testdbEntities(); // Realizamos la consulta var clienteEliminar = dbContext.Clientes.Where(cliente => cliente.IdCliente == 9).First(); // Ejecutamos el procedimiento almacenado pasando el ID (9) como parámetro. dbContext.sp_Cliente_DeleteCascade(clienteEliminar.IdCliente); // Guardamos los cambios dbContext.SaveChanges(); // Comprobamos si todo es correcto mostrando los clientes por pantalla foreach (Cliente cliente in dbContext.Clientes) { Console.WriteLine(string.Format("ID: {0}\tNOMBRE: {1}\tAÑO NAC: {2}", cliente.IdCliente, cliente.Nombre, cliente.FechaNacimiento.Year)); }
Con esto, el borrado en cascada estaría completado.
Excelente el material y muy buena la explicacion.
Genial!!!, me ha servido de mucho, gracias
Gracias por la información. ¿sabes de algun libro que pueda comprar para aprender mas sobre este framework?
Excelente. Gracias
Hola. primero excelente tu post. Tengo una duda en cuanto a «Remove», en mi caso tengo varias tablas que debo eliminar datos, el problema se da que ese dato puede o no existir en esas tablas, entonces cada vez que hago el SaveChange me da error porque claro, no encuentra datos para eliminar. Por lo pronto mi solución fué consultar si ese datos existe, de ser así hago el Remove de lo contrario paso a la siguiente tabla, pero estoy pediendo mucho tiempo en ir y venir a la base de datos que por cierto estoy trabajando con SQLITE. Como puedo resolver esto ?. Desde ya muchas gracias
Gracias por el aporte compañero!