LINQ to SQL (IV): Modificación de datos


Antes de la aparición de LINQ, la filosofía de ADO.NET estaba orientada a datos. De este modo, cuando era necesario modificar o eliminar un registro, nuestro afán era detectar la clave primaria de ese elemento y generar, bien de forma estática, bien de forma dinámica, una sentencia UPDATE o DELETE para que la fuente de datos realizase la operación pertinente sobre ella.

La aparición de los mappers objeto-relacionales como LINQ to SQL intentan hacer algo difícil: desterrar ese concepto de nuestra cabeza y obligarnos a pensar exclusivamente en objetos.

Esta diferencia, al implicar un cambio de paradigma, puede resultar duro. Para ilustrar la diferencia entre ADO.NET básico y LINQ to SQL, veamos un ejemplo.

Anteriormente podíamos usar un DataAdapter para mapear una tabla de base de datos dentro de un DataTable. Y era posible, a partir de ese DataAdapter, obtener dos copias de un mismo conjunto de datos, es decir:


            //Obtenemos la cadena de conexión de App.config o Web.config
            string cadenaConexion = ConfigurationManager.ConnectionStrings["TestDb"].ConnectionString;

            // Creamos una conexión a partir de la cadena de conexión
            using (SqlConnection conexion = new SqlConnection(cadenaConexion))
            {
                // Declaramos una consulta
                string sqlSelect = "select * from Cliente";

                // Abrimos la conexión y generamos un SqlCommand con la conexión y la consulta
                // Instanciamos un SqlDataAdapter a partir del SqlCommand
                conexion.Open();
                SqlCommand commandSelect = new SqlCommand(sqlSelect, conexion);
                SqlDataAdapter dataAdapter = new SqlDataAdapter(commandSelect);

                //Declaramos dos DataTables y los rellenamos con el DataAdapter
                DataTable dt1 = new DataTable();
                DataTable dt2 = new DataTable();

                dataAdapter.Fill(dt1);
                dataAdapter.Fill(dt2);

                conexion.Close();
            }

Con esto habremos generado dos tablas con exactamente los mismos datos:

Sin embargo, a ojos de ADO.NET, dt1 y dt2 son, pese a que contienen los mismos datos, dos objetos completamente distintos. Si realizamos cambios en los datos de una de las tablas, éstos se reflejarán exclusivamente en la tabla que hemos modificado, pero no en la otra. Entramos en el campo de la identidad de un objeto.

Si realizamos una operación similar mediante LINQ to SQL y usamos un mismo DataContext en lugar de un mismo DataAdapter, haríamos algo como lo siguiente:


            Testdb dbContext = getDataContext();

            var consulta1 = from cliente in dbContext.Cliente
                            select cliente;

            var consulta2 = from cliente in dbContext.Cliente
                            select cliente;

 

En este caso, al utilizar el mismo DataContext para extraer los datos, los datos serán el mismo elemento. Como ejemplo, obtendremos dos clientes: el primer elemento de consulta1 y el primer elemento de consulta2. Por lógica, ambos elementos tendrán los mismos datos, pero ¿será el mismo objeto o serán objetos distintos con copias idénticas de los mismos datos, tal y como ocurría con los DataTable?

Para comprobarlo, haremos el siguiente experimento: modificaremos el nombre del primer objeto y recuperaremos el nombre del segundo.


            Cliente cliente1 = consulta1.First();
            Cliente cliente2 = consulta2.First();

            cliente1.Nombre = "Carlos Alberto Gonzalez Rodriguez";

            Console.WriteLine(string.Format("Nombre: {0}", cliente2.Nombre));

Si ejecutamos esta sentencia, veremos lo siguiente:

Por lo tanto, vemos que cliente1 y cliente2 no sólo tienen los mismos datos, sino que en realidad referencian al mismo objeto: los cambios en uno se registran también en el otro, ya que en realidad, ambos son el mismo objeto.

Ahora bien, ¿qué ocurrirá en la base de datos? ¿También se habrán reflejado allí los cambios? Hagamos una consulta y comprobémoslo:

Update

No. Los cambios al cliente no se han registrado en la base de datos, ya que el cliente cuyo IdCliente es 1 no tiene el segundo nombre (Alberto) al realizar una consulta directamente sobre la fuente de datos. El motivo es que, al igual que al realizar una operación en base de datos hay que ejecutar un commit (comprometer los cambios), es necesario realizar lo mismo cuando trabajamos con LINQ to SQL. Así, el método SubmitChanges() de DataContext se encargará de realizar esta operación.


            cliente1.Nombre = "Carlos Alberto Gonzalez Rodriguez";
            try {
                dbContext.SubmitChanges();
            }
            catch (Exception ex) {
                throw (ex);
            }

Si después de realizar esta operación consultamos la base de datos, vemos que ahora los cambios realizados sobre los objetos se han comprometido.

Recordemos que este comportamiento se producirá únicamente en objetos que hayan sido recuperados por un mismo DataContext, ya que es el propio DataContext el encargado de mantener el registro de todos los objetos recuperados a través de él. Si usamos otro DataContext para recuperar el mismo elemento, estaremos hablando de dos objetos diferentes.

Una vez que el objeto ha sido cargado desde la base de datos, éste residirá en memoria, permaneciendo ajeno a los posibles cambios que se puedan realizar en la base de datos. Por ello, si queremos asegurarnos de que un objeto tiene datos válidos, podemos usar para ello el método Refresh() del DataContext, que se encargará de actualizar la información de los objetos que tengamos cargados en memoria con los datos que actualmente se encuentran en la base de datos. Así, si tenemos los siguientes fragmentos de código:


            Testdb dbContext = getDataContext();

            var consulta =  from cliente in dbContext.Cliente
                            select cliente;

            // Recuperamos el cliente con IdCliente == 1
            Cliente cliente1 = consulta.Where(c => c.IdCliente == 1).FirstOrDefault();

            // Mostramos su valor
            Console.WriteLine(string.Format("Cliente: {0}", cliente1.Nombre));

            // Realizamos una pausa para modificar el cliente en base de datos
            Console.ReadLine();

Extraeremos el cliente con IdCliente = 1 de la base de datos y lo mostraremos en pantalla. A continuación le solicitaremos al usuario que pulse ENTER para continuar con la ejecución, de modo que, en ese tiempo, podamos ejecutar la siguiente sentencia en base de datos:


    update Cliente set Nombre = 'Modificado desde Base de Datos'
        where IdCliente = 1;

Si ejecutamos esto, el cliente 1 habrá modificado su valor en la fuente de datos, tal y como vemos:

El código que vendrá a continuación se dedicará a comprobar el funcionamiento del método Refresh(). Mostraremos por pantalla nuestro objeto (cuyos datos han cambiado en base de datos pero no en memoria). A continuación realizaremos un Refresh() indicando el tipo de refresco (guardar cambios, guardar cambios actuales, recuperar todo). Le indicaremos también la consulta (listado) que queremos actualizar.

Después seguiremos jugando con el objeto, volviendo a mostrar los datos por pantalla una vez refrescados, y repetiremos el proceso realizando una actualización en la base de datos.


           // Mostramos nuevamente su valor
            Console.WriteLine(string.Format("Cliente: {0} (sin Refresh())", cliente1.Nombre));

            // Refrescamos el DataContext
            dbContext.Refresh(RefreshMode.OverwriteCurrentValues, consulta);

            // Mostramos nuevamente su valor
            Console.WriteLine(string.Format("Cliente: {0} (con Refresh())", cliente1.Nombre));

            // Cambiamos su valor y actualizamos en base de datos
            cliente1.Nombre = "Cliente modificado 2";

            // Actualizamos la base de datos
            try {
                dbContext.SubmitChanges();
            } catch (Exception ex) {
                throw (ex);
            }

            // Mostramos nuevamente su valor
            Console.WriteLine(string.Format("Cliente: {0} (tras update, sin Refresh())", cliente1.Nombre));

            // Refrescamos el DataContext
            dbContext.Refresh(RefreshMode.OverwriteCurrentValues, consulta);

            // Mostramos nuevamente su valor
            Console.WriteLine(string.Format("Cliente: {0} (tras update, con Refresh())", cliente1.Nombre));

Nuestro programa mostrará lo siguiente:

Mientras tanto, en la base de datos…

Como conclusión, podemos adivinar fácilmente que un objeto DataContext debe ser creado, utilizado y destruido una vez finalizado su trabajo, ya que de lo contrario será necesario realizar refrescos constantemente para comprobar si los datos mantenidos en memoria son coherentes con los que se encuentran en la base de datos. Dicho esto, no es difícil intuir que hacer uso de un único DataContext que se mantiene a lo largo de toda la aplicación es una mala idea en términos de concurrencia.

Modificación de las relaciones

Hemos visto que modificar una entidad es sencillo: se realiza un cambio sobre el objeto y se le pide al contexto que comprometa los cambios. Pero ¿qué ocurre si queremos cambiar, en lugar de un atributo de un objeto, la relación que un objeto tiene con otro?

Por ejemplo, imaginemos que nos hemos equivocado al realizar un pedido y que éste no pertenece a Carlos Gonzalez Rodriguez, sino que pertenece a Luis Gomez Fernandez. Lo que queremos es “mover” los objetos de tipo Pedido de un cliente a otro. ¿Es eso posible? ¿Cómo lo hacemos?

Si estamos utilizando POCOs generados de forma manual, entonces para realizar este tipo de operaciones será necesario seguir tres pasos:

  1. Hacer que el objeto referenciado deje de apuntar a su antiguo padre y pase a apuntar al nuevo (Cambiar el Cliente que contiene Pedido).
  2. Eliminar el objeto de su antiguo padre (Borrar los pedidos que ya no pertenezcan al Cliente antiguo)
  3. Añadir el objeto a su nuevo padre (Añadir los pedidos que pertenezcan al nuevo Cliente).

Ilustrándolo con un ejemplo, imaginemos que el pedido 4, que pertenece a Carlos Gonzalez Rodriguez (cliente con IdCliente = 1), ha de ser transferido a Luis Gomez Fernandez (cliente con IdCliente = 2). Lo inmediato sería pensar en cambiar el campo IdCliente del objeto Pedido para que cambie su valor de 1 a 2. Sin embargo, no estamos trabajando con un modelo relacional, sino con objetos, así que nunca ha de modificarse la clave foránea de forma manual. Hay que realizar esta operación con un paradigma objetual.

Por lo tanto, lo que haremos será lo siguiente:


            // Obtenemos los clientes 1 y 2
            Cliente cliente1 = (from cliente in dbContext.Cliente.
                                    Where(c => c.IdCliente == 1)
                                select cliente).FirstOrDefault();

            Cliente cliente2 = (from cliente in dbContext.Cliente.
                                    Where(c => c.IdCliente == 2)
                                select cliente).FirstOrDefault();

            // Cambiamos la referencia del Pedido 4 para que apunte a su nuevo padre (Cliente 2)
            Pedido pedido4 = cliente1.Pedido.Where(pedido => pedido.IdPedido == 4).FirstOrDefault();
            pedido4.Cliente = cliente2;

            // Eliminamos el pedido del cliente 1
            cliente1.Pedido.Remove(pedido4);

            // Añadimos el pedido al cliente 2
            cliente2.Pedido.Add(pedido4);

            // Comprometemos los cambios
            try {
                dbContext.SubmitChanges();
            }
            catch (Exception ex) {
                throw (ex);
            }

El resultado será que el mapper actualizará automáticamente el pedido:

El paso más importante de todos es, con diferencia, asociar el nuevo cliente al pedido, es decir, cambiar la referencia. Si únicamente realizamos ese paso ignorando los otros dos y guardamos los cambios, el resultado será el mismo. Sin embargo los dos pasos adicionales (eliminar listado del anterior registro y asignárselo al nuevo) es una buena práctica de programación debido a que, si bien LINQ to SQL va a entender la operación si únicamente cambiamos la referencia, el estado de los objetos no será consistente con la base de datos, ya que hasta que no se realice una operación Refresh() o similar, nuestro DataContext() seguirá enlazando los objetos tal y como los recuperó de la base de datos.

El proceso se simplifica si hacemos uso de la clase generada por SqlMetal o por el DataContext Editor. En ese caso, será tan fácil como realizar lo siguiente:


           // Añadimos el pedido al cliente 2
            cliente2.Pedido.Add(pedido4);

            // Comprometemos los cambios
            try {
                dbContext.SubmitChanges();
            }
            catch (Exception ex) {
                throw (ex);
            }

Por lo tanto, si usamos alguna de estas clases, añadir el pedido 4 al cliente 2 se encargará de realizar automáticamente los tres pasos anteriores por nosotros. Para que esta magia funcione, el objeto Pedido ha de consistir en un EntitySet de pedidos, de la siguiente forma:


		[global::System.Data.Linq.Mapping.AssociationAttribute(Name="Cliente_Pedido", Storage="_Pedido", ThisKey="IdCliente", OtherKey="IdCliente")]
		public EntitySet Pedido
		{
			get
			{
				return this._Pedido;
			}
			set
			{
				this._Pedido.Assign(value);
			}
		}

El código que realiza esta operación de eliminación y adición también se encuentra en nuestro DataContext. Específicamente en los siguientes delegados, encargados de añadir y eliminar el Cliente de nuestro pedido:


		private void attach_Pedido(Pedido entity)
		{
			this.SendPropertyChanging();
			entity.Cliente = this;
		}

		private void detach_Pedido(Pedido entity)
		{
			this.SendPropertyChanging();
			entity.Cliente = null;
		}

Por lo tanto, si quisiéramos asociar , por ejemplo, todos los pedidos asociados al cliente 2 al cliente 1, bastaría con hacer lo siguiente:


            cliente1.Pedido.AddRange(cliente2.Pedido);

            try {
                dbContext.SubmitChanges();
            }
            catch (Exception ex) {
                throw (ex);
            }

La base de datos, nuevamente, nos informa de que el proceso ha sido satisfactorio

Insert

La inserción es un proceso más sencillo: basta con crear los objetos y usar el método Add para realizar la inserción. Así, si quisiéramos añadir un nuevo pedido consistente en dos bolígrafos al cliente 5, haríamos lo siguiente:


            // Obtenemos el cliente 5 y el producto boligrafo
            Cliente cliente5 = (from cliente in dbContext.Cliente.
                                    Where(c => c.IdCliente == 5)
                                select cliente).FirstOrDefault();

            Producto boligrafo = (from producto in dbContext.Producto.
                                    Where(p => p.Descripcion.Equals("Boligrafo"))
                                  select producto).FirstOrDefault();

            // Creamos un nuevo pedido asociado al cliente 5
            Pedido pedido = new Pedido
            {
                FechaPedido = DateTime.Now
            };

            // Creamos una nueva línea de pedido consistente en dos bolígrafos
            LineaPedido lineaPedido = new LineaPedido
            {
                Producto = boligrafo,
                Cantidad = 2
            };

            // Añadimos la línea de pedido al pedido
            pedido.LineaPedido.Add(lineaPedido);

            // Añadimos el pedido a la lista.
            cliente5.Pedido.Add(pedido);

            // Comprometemos los cambios
            try {
                dbContext.SubmitChanges();
            }
            catch (Exception ex) {
                throw (ex);
            }

También es posible, si conocemos la clave foránea del objeto al que referencia o no tenemos el objeto disponible en memoria, indicar este valor y omitir añadir el objeto a la colección. Es decir, en lugar de esto:


            Pedido pedido = new Pedido
            {
                FechaPedido = DateTime.Now
            };
            cliente5.Pedido.Add(pedido);

Es posible realizar esto, que tendrá el mismo resultado:


            Pedido pedido = new Pedido
            {
                FechaPedido = DateTime.Now
                IdCliente = 5;
            };

Regresando a la base de datos, vemos que los elementos se han insertado correctamente:

Ahora bien, ¿qué ocurriría si en lugar de añadir un objeto a una lista de otro objeto (en este caso, un pedido a un cliente que ya existía) insertáramos un registro nuevo directamente? Para hacer eso utilizaríamos el método InsertOnSubmit(), tal y como mostramos en el siguiente código, en el que creamos un cliente, un pedido y una línea de pedido:


            Producto lapicero = (from producto in dbContext.Producto.
                                    Where(p => p.Descripcion.Equals("Lapicero"))
                                  select producto).FirstOrDefault();

            // Creamos un nuevo cliente
            Cliente cliente = new Cliente {
                Nombre = "Lorena Gago Silva",
                FechaNacimiento = new DateTime(1982, 2, 4)
            };

            // Creamos un nuevo pedido
            Pedido pedido = new Pedido
            {
                FechaPedido = DateTime.Now
            };

            // Creamos una nueva línea de pedido consistente en diez lapiceros
            LineaPedido lineaPedido = new LineaPedido
            {
                Producto = lapicero,
                Cantidad = 10
            };

            // Añadimos la línea de pedido al pedido
            pedido.LineaPedido.Add(lineaPedido);

            // Añadimos el pedido al cliente
            cliente.Pedido.Add(pedido);

            // Comprometemos los cambios
            try
            {
                dbContext.Cliente.InsertOnSubmit(cliente);
                dbContext.SubmitChanges();
            }
            catch (Exception ex)
            {
                throw (ex);
            }

Delete

Mientras que añadiendo un objeto a un listado mediante el método Add() y comprometiendo los cambios podemos conseguir una inserción, la versión opuesta Remove()
no elimina un registro al comprometer la operación. Si queremos borrar un elemento deberemos utilizar el método DeleteOnSubmit() de forma explícita, asegurándonos de eliminar también todos aquellos registros que dependan del registro que estamos eliminando.

Así, si quisiésemos eliminar el cliente junto a todos sus pedidos y sus líneas de pedido, ejecutaríamos la siguiente secuencia:


            Cliente cliente6 = (from cliente in dbContext.Cliente
                                where cliente.IdCliente == 6
                                select cliente).First();

            // Borramos el cliente
            dbContext.Cliente.DeleteOnSubmit(cliente6);

            // Por cada pedido del cliente...
            foreach (Pedido pedido in cliente6.Pedido)
            {
                // Eliminamos el pedido
                dbContext.Pedido.DeleteOnSubmit(pedido);

                // Eliminamos todas las líneas asociadas al pedido
                dbContext.LineaPedido.DeleteAllOnSubmit(pedido.LineaPedido);
            }

            dbContext.SubmitChanges();

Dentro del foreach en el que recorremos todos los pedidos del cliente estamos realizando dos operaciones:

  • DeleteOnSubmit(pedido): eliminamos el pedido actual
  • DeleteAllOnSubmit(pedido.LineaPedido): eliminamos todas las líneas de pedido del pedido actual

Lo cual sería equivalente a anidar otro foreach dentro del primero que se encargara de ello, del modo siguiente:


            foreach (Pedido pedido in cliente6.Pedido)
            {
                // Eliminamos el pedido
                dbContext.Pedido.DeleteOnSubmit(pedido);

                // Eliminamos todas las líneas asociadas al pedido
                foreach (LineaPedido lineaPedido in pedido.LineaPedido)
                {
                    dbContext.LineaPedido.DeleteOnSubmit(lineaPedido);
                }
            }

Además, el orden de eliminación, al contrario de lo que ocurre en SQL, no es importante, ya que será LINQ quien se encargue internamente de colocar las sentencias para su correcta ejecución. Lo que no debemos olvidar es dejarnos registros huérfanos (como olvidarnos de borrar las líneas de pedido al eliminar un pedido).

Si queremos olvidarnos de tener que hacer esto de forma manual, siempre podemos activar el borrado en cascada adornando la asociación en el fichero generado por SqlMetal o Datacontext Editor mediante el parámetro DeleteRule=”cascade”, tal y como se muestra a continuación:


		[global::System.Data.Linq.Mapping.AssociationAttribute(Name="Cliente_Pedido", Storage="_Pedido", ThisKey="IdCliente", OtherKey="IdCliente", DeleteRule="cascade")]
		public EntitySet Pedido
		{
			get
			{
				return this._Pedido;
			}
			set
			{
				this._Pedido.Assign(value);
			}
		}

Transacciones y concurrencia

Para finalizar con LINQ to SQL, hablemos un poco acerca de la concurrencia, es decir, de la posibilidad de que más de una operación afecte a un mismo conjunto de datos en el mismo instante de tiempo.

Los métodos más comunes para plantear un buen control de concurrencia suelen ser tres:

  • Bloqueo exclusivo de la tabla
  • Concurrencia optimista
  • Campo “versión”

El primer método consiste en realizar un Lock sobre la tabla antes de ejecutar una operación de consulta. Este proceso, aunque efectivo, implica una pérdida de rendimiento que no todos los sistemas pueden soportar.

La concurrencia optimista, como su nombre indica, consiste en “esperar que todo vaya bien”. Se realiza una comprobación antes de modificar un campo, consistente en comparar el valor actual del campo y el que tenía cuando se recuperó (antes de la modificación). Este tipo de concurrencia se codifica a nivel de columna, usando para ello el parámetro UpdateCheck, que puede ser:

  • Always (siempre se comprueba el campo al ir a realizar una escritura)
  • Never (el campo nunca se comprueba al ir a realizar una escritura)
  • WhenChanged (el campo sólo se comprueba si se va a cambiar su valor en la base de datos).

		[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_FechaNacimiento", DbType="Date NOT NULL", UpdateCheck=UpdateCheck.WhenChanged)]
		public System.DateTime FechaNacimiento
		{
			get
			{
				return this._FechaNacimiento;
			}
			set
			{

Finalmente, siempre es posible añadir un campo “version” a cada tabla que se incremente en una unidad cada vez que se efectúa una operación sobre el registro (o que almacene la fecha de la última modificación. Comparando el valor actual con el recuperado (por ejemplo, añadiéndole UpdateCheck=UpdateCheck.Always) es posible averiguar si el campo ha sido modificado entre la recuperación del registro y su escritura. Esta última versión es óptima para tablas con un gran número de columnas, en las que comprobar todas y cada una de ellas podría resultar tedioso. Este campo se adorna con los parámetros IsVersion=true, IsDbGenerated=true.


		[global::System.Data.Linq.Mapping.ColumnAttribute(Name="version", IsDbGenerated=true, IsVersion=true)]
		public Binary Version
		{ get; set; }

En cuanto a las transacciones, al igual que en ADO.NET, es posible encapsularlas dentro de una transacción para que todas las operaciones realizadas se comporten de forma atómica. Para ello habrá que referenciar, en primer lugar, el espacio de nombres System.Transactions.


                using (TransactionScope transaccion = new TransactionScope())
                {
                    Cliente cliente = new Cliente() { Nombre = "Pedro Martin Angulo", FechaNacimiento = new DateTime(1988, 12, 12) };

                    Cliente cliente2 = (from c in dbContext.Cliente
                                       where c.IdCliente == 2
                                       select c).First();

                    cliente.Nombre = "Juan Lopez Lopez";
                    dbContext.Cliente.InsertOnSubmit(cliente);

                    dbContext.SubmitChanges();
                }

En este caso hay que destacar un pequeño detalle: la transacción es inútil. La razón es que el método SubmitChanges() actúa de forma atómica sobre todas las operaciones realizadas sobre el DataContext hasta ese momento, por lo que a efectos prácticos, se comportará como una transacción.

Las transacciones, por tanto, deberán reservarse para otras situaciones, como en las que varias fuentes de datos (o varios DataContext) estén implicados en la operación.

Deja un comentario

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. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s