[HowTo] – LINQ to SQL: Mostrando el progreso de Context.SumbitChanges()
Hola de nuevo,
En el proyecto que me ocupa actualmente he tenido que pelearme con algo curioso: Resulta que hay que realizar un proceso que lea una serie de ficheros de texto (si, si, de texto) que contienen una serie de información que debo analizar y posteriormente volcar a la base de datos. Hasta aquí ningún problema, salvo el de tener que lidiar con unos ficheros de texto cuyos formatos parecen haber sido diseñados por un loco (parece mentira que hoy en día hayan tantas organizaciones / empresas / bancos que sigan utilizando este método para intercambiar información).
El problema
Una vez desarrolladas las clases necesarias para leer y desglosar toda la información, me he encontrado con que se generan en el menor de los casos del orden de medio millón de filas a insertar en la BD.
Como os podéis imaginar el tiempo para realizar el volcado es considerable, del orden de entre 5 y 15 minutos. Así que he pensado en al menos optimizar la experiéncia del usuario, y me he marcado estos objetivos:
-
Que el usuario pueda seguir trabajando mientras se insertan los registros
-
Poder mostrar el progreso de la operación
Partimos de que tenemos un contexto de LINQ to SQL (por favor, no me preguntéis porque estamos usando esto en lugar de EF o NHibernate). En dicho contexto tenemos unas 495.000 filas pendientes de insertarse en cuanto hagamos la llamada a SubmitChanges(), y lo malo es que este método es síncrono y además no genera ningún evento ni permite usar callbacks ni ningún mecanismo de notificación del progreso.
Solución al primer problema (asincronía)
Es el más sencillo de los dos con diferencia. En versiones anteriores debíamos crear un thread manualmente, pero desde la aparición de la clase Task en .NET 4.0 esto se ha simplificado enormemente. Basta con crear un objeto Task que apunte al método que deseamos lanzar de forma asíncrona (mediante paralelismo).
importEngineInsertTask = new Task(() => insertPendingRows(), cancellationTokenSource.Token);
importEngineInsertTask .Start();
Una de las ventajas respecto a los diferentes métodos de versiones anteriores es la posibilidad de pasar un token de cancelación, para por ejemplo poder cancelar la tarea si el usuario así lo decide. Tal vez sea interesante un post sobre esto más adelante, aunque es bastante sencillo.
Solución al segundo problema (mostrar progreso)
Este ha sido un poco más peliagudo, ya que en principio no hay modo de interactuar con el DataContext de LINQ to SQL para reportar el progreso de las inserciones. Para poder mostrar el progreso necesitamos saber por un lado el número total de filas a insertar y por otro el elemento actual. Para conocer el número total de filas a insertar basta con preguntarle al DataContext:
numInsertLines = context.GetChangeSet().Inserts.Count;
Pero para saber el número de insert actual no disponemos de nada. Sin embargo, el Contexto dispone de una propiedad Log de tipo TextWriter, que permite la monitorización de cada operación. Bien, a partir de esto, podemos generar una clase propia que herede de TextWriter:
class ActionTextWriter : TextWriter
{
private readonly Action<string> action;
public ActionTextWriter(Action<string> action)
{
this.action = action;
}
public override void Write(char[] buffer, int index, int count)
{
Write(new string(buffer, index, count));
}
public override void Write(string value)
{
action.Invoke(value);
}
public override Encoding Encoding
{
get { return System.Text.Encoding.Default; }
}
}
Y posteriormente usarla para llamar a un método que se encargue de lanzar un evento que será monitorizado para mostrar el progreso
private void configureSubmitInsertProgress()
{
context.Log = new ActionTextWriter(s =>
{
if (s.StartsWith("INSERT INTO"))
ReportInsertProgress(s);
});
}
Conclusión
El resultado es que el usuario puede realizar un proceso relativamente pesado sin tener que dejar de trabajar, ya que el proceso se ejecuta en segundo plano, y además puede ver un proceso del progrso de la operación. Ya sabemos lo impacientes que pueden ser los usuarios, verdad? 🙂
Saludos,
6 Responsesso far
Hola Lluis, un articulo muy interesante, me gustaria saber como haces el progreso de la monitarizacion, viendo tu codigo creo que se realiza en el metodo ReportInsertProgress(s).
Saludos.
Un poco complicado usar LinqToSQL para esto no? Con un Command sobra… 🙂
Por lo demás mola mucho la ocurrencia para mostrar el progreso!!!
Un saludo titán.
@Sergio: Si, en el método ReportInsertProgress se lanza un evento que será interceptado por el ‘consumidor’ de la clase. Sólo hay que tener en cuenta que los eventos provienen desde un thread que no es el principal, con lo que no se puede acceder directamente a la interfaz de usuario. A ver si saco un ratito y posteo un ejemplo más completo 🙂
@Rodri: Calla, calla… ya lo se. Yo no hubiese utilizado LINQ to SQL :-/ El tema es (además de que he simplificado un poco el ejemplo) viene impuesto en el diseño. Es un MUST DO o como decimos aquí POR COJONES 😉
Me apunto tu solución al «PC». 🙂
Nunca sabes cuando te puedes encontrar con una de estas… pero… no digas nunca jamás.
Muy buena entrada y muy muy bien pensado tito. 🙂
Buenas!
Una solución de lo más ingeniosa, enhorabuena :-))
Un saludo.
interesante, señor, grazie; sí, un ejemplo más completo sería clarificador. salu2grz