Entity Framework (III): Select, Insert, Update, Delete


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.

6 comentarios

  1. 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

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s