viernes, 13 de julio de 2007

Control de concurrencia pesimista con ADO.NET (C#)

Esta es la primera entrada seria del blog, después de la presentación, así que espero que me salga bien.

El tema a tratar es el control pesimista en ADO.NET como solución para la concurrencia entre varios usuarios al acceder a un mismo registro de una tabla. El caso concreto que voy a tratar corresponde a una base de datos SQL Server. Para otro tipo de bases de datos existen otras soluciones.

Para los que no conozcáis como maneja la concurrencia ADO.NET tenéis una introducción en este blog, o en esta entrada de MSDN.

En este post vamos a tratar el caso de la concurrencia pesimista, esto es, cuando un usuario accede a un registro éste quedará bloqueado para los demás usuarios que tengan acceso a la base de datos.

Para que os hagáis una idea de por donde van los tiros, se trata de especificarle a SQL Server que queremos bloquear un registro mientras estamos en una transacción.

¿Cómo se hace?

Vayamos por partes, lo primero que tengo que explicar es que en SQL Server existe una instrucción en la sentencia SELECT para que bloquee un registro cuando lo leemos. Esta instrucción se conoce como Table Hint y lo que hace es indicarle al optimizador de consultas (query optimizer) el método de bloqueo de la tabla o vista.

Por emplo: SELECT * FROM MiTabla WITH(Rowlock, Xlock) WHERE id = 10;

Rowlock -> fuerza el bloqueo de los registros que devuelva la instrucción.
Xlock -> indica que el bloqueo es exclusivo.

Y esta sentencia SELECT la tenemos que iniciar con una transacción abierta. El bloqueo existe mientras la transacción esté en curso, por tanto, hasta que no se finalice la transacción (Commit o Rollback), se salga de su alcance o se cierre la conexión, el bloqueo permanecerá activo. Para que el bloqueo sea efectivo el nivel de asilamiento de la transacción (isolationLevel) puede ser ReadCommitted, ReadUnCommitted, RepetableRead o Serializable, no aceptandose Chaos, ni SnapShot, ni Unspecified.



El código

Seguro que viendo el código lo entenderéis mejor de que va todo esto, así que aquí lo tenéis:




using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace BloqueoPesimista
{
class Program
{
static void Main(string[] args)
{

DataSet ds = new DataSet();
SqlConnection conexion = new SqlConnection();
SqlTransaction transaccion;
SqlCommand comando = new SqlCommand();
SqlDataAdapter da = new SqlDataAdapter();

Console.WriteLine("Pulse una tecla para iniciar...");
Console.ReadKey();

try
{

// La cadena de conexión indica que atacamos a la base de datos Northwind del servidor local
conexion = new SqlConnection("Data Source=.;Initial Catalog=Northwind;Integrated Security=True");

comando.Connection = conexion;
comando.CommandType = CommandType.Text;

// El SELECT tiene un parámetro para evitar SQL Injection
comando.CommandText = "SELECT ProductID, ProductName FROM Products WITH (Rowlock,Xlock) " +
"WHERE ProductID = @ID";
comando.Parameters.AddWithValue("@ID", 1);
da.SelectCommand = comando;

//Abrimos la conexión y ejecutamos en comando
//Nota: también se puede hacer con la sentencia Using() que nos evita abrir y cerrar la conexión
conexion.Open();

// La transacción no tiene constructor, se crea a partir de la conexión a través de un Class Factory
transaccion = conexion.BeginTransaction(IsolationLevel.ReadCommitted);

// Una vez creada la transacción la asignamos al comando
comando.Transaction = transaccion;

// Ejecutamos el comando mediante el SQLDataAdapter
da.Fill(ds, "Products");

// Mostramos el registro bloqueado
Console.WriteLine("Registro con ProductID = {0} está bloqueado.", ds.Tables["Products"].Rows[0]["ProductID"]);

Console.WriteLine("Pulse una tecla para finalizar el bloqueo...");
Console.ReadKey();

// Finalizamos la transacción, aquí ya se deshace el bloqueo
transaccion.Rollback();

// Cerramos la conexión
conexion.Close();
}
catch (Exception e)
{
if (conexion.State != ConnectionState.Closed)
conexion.Close();

Console.WriteLine(e.Message);
Console.ReadKey();
}
}
}
}



Se trata de una aplicación de consola en C#, en un futuro post pondré el código en VB.NET. Así que sólo hay que copiarlo en un proyecto de consola.


Vamos a probarlo

Para poder pobrarlo, hay que Generar el proyecto, con lo que nos creara un fichero .EXE en la carpeta Bin/Debug del proyecto.
Ejecutamos el exe una vez y pulsamos una tecla para que se inicie el bloqueo.







Si volvemos a ejecutar el exe e iniciamos el bloqueo, la aplicación se quedará esperando que se libere el registro.






En cuanto finalicemos la primera ejecución, la segunda podrá acceder al registro y bloquearlo.


Y con esto finalizamos por hoy. Para ser la primera entrada me ha quedado un poco larga, pero espero que pueda ayudar a cualquiera de los que la lean.

Cualquier duda o sugerencia, por favor, en los comentarios.


PD: ¿Es el control de concurrencia pesimista la mejor opción? En absoluto, pero para algún caso en concreto puede ser LA SOLUCIÓN. Queda a vuestro criterio el usarlo o no.

2 comentarios:

Jorge dijo...

EXCELENTE Y CLARA EXPLICACIÓN, NO OBSTANTE, APLIQUÉ EL MÉTODO EXPLICADO Y A PESAR DE QUE BLOQUEO EL PRIMER REGISTRO NO ME DEJA ACCESAR EL SEGUNDO REGISTRO DESDE OTRA APLICACIÓN CUANDO EN REALIDAD EL QUE ESTÁ BLOQUEADO ES EL PRIMERO. ES COMO SI BLOQUEARA TODA LA TABLA, SALUDOS, JORGE GARCELL.

pbousan dijo...

Gracias Jorge por el alago.

En principio debería funcionar bien, si quieres mándame tu código y lo reviso.

Un saludo