Wednesday, July 20, 2011

LINQ to SQL. La Cache, ¿una ventaja o un problema? (parte 2)


Haciendo un buen resumen creo que me ha traído más problemas que ventajas aun cuando las ventajas sean medio invisibles por su propia naturaleza. A lo mejor una consulta es menos eficiente sin el uso del Cache pero no lo vemos.
La cuestión es que LINQ se encarga de manipular lo que ya tiene en memoria y lo que trae de la BD ante una consulta y no nos deja muchas opciones al respecto. Aun cuando existe una propiedad supuestamente bajo la cual le podemos decir al DataContext de LINQ que no utilice el Cache pues solo es funcional para datos de solo lectura.
Realmente a mí me gustaría poder decirle: No uses el Cache, tráeme siempre datos frescos.
El problema es simple, veámoslo en una secuencia de acciones, en donde todas usan un mismo objeto DataContext: 
  1. La instancia1 de la aplicación lee la existencia de un producto X = 0. LINQ lo mantiene en Cache.
  2. La instancia2 también lo lee. X = 0. LINQ lo mantiene en Cache.
  3. La instancia1 consulta de  nuevo la existencia de X y le suma 2. X = 2.
  4. La instancia2 consulta de  nuevo la existencia de X y le suma 3. Aquí debería queda X = 3 pero no, resulta que es igual a 3.
 ¿Cuál es el problema?
Que en el paso 4 cuando la instamcia2 consulta el valor de la existencia del producto X el Cache le devuelve el valor 0, aun cuando ya la instancia1 lo ha modificado.
Cuando al DataContext le solicitan datos lo hace a través de su Cache. Si el registro (siguiendo como criterio la llave de la Entidad) ya existe en la Cache entonces no lo consulta desde la BD sino que lo retorna desde allí mismo. Si no existe (solo si no existe) en la Cache entonces lo trae de la BD.
Cuando trabajamos en ambiente web con ASP.Net, sumado a que casi siempre seguimos un patrón Singleton para el DataContext general de la aplicación, no trae problema alguno. Pero en aplicaciones concurrentes es un gran problema.
Una solución es utilizar un DataContext nuevo cada vez que necesitamos datos frescos pero eso desde el punto de vista de la misma lógica no es siempre posible pues necesitamos los datos que ya tenemos y que no estarían en el nuevo DataContext.
Otra solución que encontramos, que usamos un tiempo, y que ni siquiera publicaré porque no me gusta, es crear un mecanismo (un truco) de limpieza del Cache del DataContext que usaremos cuando lo necesitemos, por ejemplo, antes de efectuar el paso 4 de la secuencia de acciones. Con esto obligamos al DataContext a traer de la BD el valor actual.
Pero aquí es donde el desconocimiento nos juega una mala pasada pues existe en el DataContext un método para ello, aun cuando esté poco documentado: Refresh.
public void Refresh(
        RefreshMode mode,
        IEnumerable entities
)

Cuando necesitemos obtener una copia fresca de los datos, por ejemplo de la tabla de Inventarios, podemos usar una sentencia similar a la siguiente justo antes de hacer uso de ella:
dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Inventories);

var inv = dc.Inventories.Where(i => i.IdProduct == op.IdProduct && i.IdLocation == op.IdLocation).SingleOrDefault();

Ahora, aun cuando esto soluciona definitivamente el problema de obtener datos frescos de la BD nos produjo una seria de problemas, algunos “mágicos” y por ende se nos iban de control.
El primero de ellos es que con una tabla de Clientes de 800 mil registros, en la cual hacíamos una búsqueda elemental, el sistema se demoraba una eternidad. Increíblemente el proceso de Refrescado casi congelaba la aplicación y no me pregunten por qué? Porque no tengo respuestas. En este caso era más factible realizar la búsqueda sobre un DataContext nuevo que sobre el existente y la velocidad era la normal. 
El segundo problema más serio aun es que el Refresh nos generaba una excepción cuando lo usábamos en un entorno de Transacciones como el mencionado arriba (parte1 de este artículo). La excepción nos decía algo así como que la conexión o la transacción no coincidía, etc, etc. Nada, un bugs que busqué un par de veces y no encontré un motivo. 
En el proceso de búsqueda de soluciones encontré una biblioteca de .Net específica para el tratamiento de excepciones. Una clase destinada a tales efectos: TransactionScope, que trataremos en la tercera parte de este artículo.