NavarraDotNet:Windows Server AppFabric Cache

Este jueves 25 tengo la suerte de volver a estar con la gente de NavarraDotNet, que más que un grupo de usuarios, es ya un grupo de amigos. En esta ocasión en el evento trataremos de dar un vistazo, todo lo profundo que el tiempo nos deje a “Velocity”, para una descripción más formal de la agenda y el registro visitar la web del Centro de Excelencia de Software de Navarra.

 

Nos vemos,

Unai,

STE, las entidades duplicadas y el … vamos a ver!

La verdad es que es un tema sobre el que llevaba tiempo deseando escribr, aunque siempre he encontrado una excusa para no hacerlo, la última, que una persona más capaz que yo lo había hecho, en concreto Diego Vega, Product Manager de EF su su propio blog. Ahora, después de ver unos cuantos tweets de personas artas de problemas de duplicados  y otras que los tuvieron y no pensaron el porqué, me he decido por fin a hablar sobre el mismo.

Pongámonos en contexto sobre el problema con nuestras Selft Tracking Entities y las entidades duplicadas, o más concretamente con una excepción de tipo InvalidOperationException que se lanza en el método ApplyChanges cuando intentamos aplicar los cambios realizados en cliente. En concreto el mensaje de esta excepción es el siguiente:

“AcceptChanges cannot continue because the object’s key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges.”

Tal y como podemos ver, el mensaje de la excepción nos está indicando que tenemos elementos con clave duplicada dentro del grafo sobre el que estamos intentando aplicar cambios, pero ¿como es posible esto? ¿por que se da esta casuística?

El funcionamiento de STE es realmente simple, básicamente cada entidad dispone de un mecanismo, ChangeTracker, capaz de almacenar la información de las operaciones de cambio que sobre ella se han producido de forma desconectada de un contexto. Posteriormente, podemos coger esta información, mediante el método ApplyChanges, y llevarla al ObjectStateManager del contexto de trabajo asociado. Una vez que esta información está en el ObjectStateManager, el proceso sigue el mismo camino que si el trabajo con la entidad fuera “conectado” a un contexto. Veamos a continuación un ejemplo simple, que no trivial, para comprobar el funcionamiento, la problemática y las posibles soluciones de la misma. Para ello, partiremos del siguiente modelo:

 

fig2

Sobre el mismo, lógicamente despues de aplicar el artefacto de generación con STE, podemos ejecutar el siguiente escenario:

//customer is serialized-deserialized in GetCustomerFromServer, similar in WCF

Customer customerInClient = GetCustomerFromServer();

//add new order and also a new country
customerInClient.Orders.Add(new Order()
{
    IdOrder = 1,
    Total = 100M,
    Country = new Country()
    {
        IdCountry = 1,
        Name = “Spain”
     }
});

Save(customerInClient);

 

dónde el método Save es tan simple como sigue:

Como puede observar, en el escenario anterior, se recupera una entidad Customer del servidor, después, de forma desconectada, se agrega un pedido y en ese pedido un nuevo pais. Todo correcto, si pusiéramos un punto de ruptura justamente despues de la ejecución de ApplyChanges y observaramos el contenido del ObjectStateManager, podríamos ver con un simple visor HTML como este contexto contendría la siguiente información:

Fig1

Bien, parece que el trabajo ha funcionado y las operaciones que se llevarán a la base de datos son correctas. Cambiemos entonces nuestro primer escenario y hagamos el siguiente plan:

 

En este nuevo plan, obtendremos nuestra entidad Customer del servidor, incluyendo su información relacionada, en este caso un Order y su correspondiente Country asociado. A mayores, obtenemos el primer Country de nuestra base de datos. A la entidad cliente anterior, le agregaremos un nuevo pedido en el que estableceremos que el Country asociado es el que hemos recuperado de la base de datos. Una vez hecho esto, llamaremos a nuestro método Save, en esta llamada podremos ver la excepción comentada justamente al principio de este post….

 

¿Cual es el problema? Porque tenemos dos entidades duplicadas? Pues en nuestro caso, las entidades duplicadas que tenemos se corresponden al elemento Country almacenado en el primer pedido( el insertado en el primer plan) de nuestro cliente, y el elemento Country que estamos asociando al pedido nuevo a agregar. ¿Es lógico este problema? Pues si, por dos motivos principales, el primero es técnico ya que como usted sabe el ObjectStateManager no puede tener dos ObjectStateEntry con la misma clave de identidad, que es lo que pasaría en este caso. El segundo motivo es porque si hacemos esto estamos rompiendo la consistencia del grafo, piense que tendriamos dos entidades iguales, dos country con el mismo identificador, el mismo pais vamos, y sin embargo tienen referencias distintas.

Bien, llegados hasta aquí, ya hemos visto el problema y como reproducirlo. Los motivos por los que se suele producir este problema, se suelen deber a la presencia de grafos muy extensos, con numerosas propiedades de navegación, de forma general, modelos de entidades en los que no se ha hecho el trabajo de definir los distintos agregados y de separar los mismos.

NOTA OFF TOPIC: Me ha llamado especialmente la atencion una persona en un de estos tweets que decia que como esto le había dado problemas se había pasado a POCO y DTO. Buff, en ese mismo momento me pregunté como una persona argumenta tener la suficiente madurez para implementar este escenario y carece de la misma para hacer Aggregate Roots y librarse de este problema en STE! No se , yo creo que por eso no tengo twitter, para no tener que oir ciertas cosas a diario….

Ahora el vamos a ver,  o lo que es lo mismo, el vamos a arreglar esto. Posibles soluciones

a) Manten la consistencia del grafo: Tu debes de preocuparte de darle sentido, es decir, si las entidades tienen identidad tendrás que preocuparte de la misma…

b) Haz un MergeWith… ok?, a veces aunque te quieras preocupar de la identidad, si estás trabajando con UI, tienes pintadas las entidades, los dropdown, y en determinadas acciones vas trabajando en tu grafo, es dificil realizar esta gestión. Para solucionar/aliviar este problema podríamos hacer un método que se encargara de decirnos si un elemento con una identidad está en un grafo, si está sustituir el nuestro por eso y si no dar el nuestro. Para nuesetro caso sería algo similar a lo siguiente:

Puede obtener la implementación completa de este método, MergeWith, del proyecto de codeplex Microsoft NLayer App. Para utilizarlo, tendríamos que cambiar el plan 2  tal y como se ve a continuación:

En este nuevo plan, la asignación del nuevo pais, se hace chequeando previamente mediante el método MergeWith si ya está previante en el grafo, caso en el que se sustituiría countryInClient por el existente.

 

c) Usa las Foreign Key Properties, en nuestro caso, establece IdCountry= 1 y no Country = countryInClient como vemos a continuación en el plan 4.

 

Espero con este  post aportar un granito de arena a las personas que tenga y/o estén sufriendo este problema y no sepan el porqué y como resolverlo..

 

Saludos
unai

EF 4.0 Performance Tips #8

Sin lugar a dudas este es el tip de rendimiento más rebuscado de todos los visto hasta ahora, alguno podrá decir que ya los había un poco rebuscados, pero bueno, esta es mi opinión. Para tratar de explicarnos nos pondremos en situación. Supongamos que tenemos una tabla con una columna de tipo varchar, es decir, una columna no unicode, tal y como podría ser la siguiente:

CREATE TABLE [dbo].[NonUnicodeTest](
    [idTest] [int] NOT NULL,
    [field] [varchar](10) NOT NULL,
CONSTRAINT [PK_NonUnicodeTest] PRIMARY KEY CLUSTERED
(
    [idTest] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

 

Los que conocéis SQL sabéis que si realizamos una consulta con un filtro como WHERE field=N’ valor del campo’ podríamos tener un problema de NO uso de índices ya que esta consulta no es SARGABLE, hay una conversion de valor no unicode a valores unicode. Lo lógico es que si estamos usando Entity Framework el se ocupe por nosotros de no cometer estos errores, vamos a comprobarlo.

Si ejecutamos la siguiente consulta en Linq To Entities;

 

El SQL que llega a la base de datos es el siguiente:

 

exec sp_executesql N’SELECT
[Extent1].[idTest] AS [idTest],
[Extent1].[field] AS [field]
FROM [dbo].[NonUnicodeTest] AS [Extent1]
WHERE [Extent1].[field] = @p__linq__0′,N’@p__linq__0 varchar(8000)’,@p__linq__0=’field value’

 

Efectivamente, por ahora todo va bien, ADO.NET EF  resuelve este problema de forma correcta, es más que ese campo sea unicode o no es una atributo de la propiedad en el modelo conceptual, CSDL.

 

Untitled

Todo hace indicar que todo funcionará de forma correcta y este tipo de casuísticas está bien resuelto. Sin embargo vamos a probar con dos consultas adicionales:

 

Si examinamos las trazas con un profiler de SQL para estas dos consultas veremos los siguientes resultados, en el mismo orden.

 

exec sp_executesql N’SELECT
[Extent1].[idTest] AS [idTest],
[Extent1].[field] AS [field]
FROM [dbo].[NonUnicodeTest] AS [Extent1]
WHERE [Extent1].[field] IN (@p__linq__0,@p__linq__1)’,N’@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)’,@p__linq__0=N’field1′,@p__linq__1=N’field2′

 

 

exec sp_executesql N’SELECT
[Extent1].[idTest] AS [idTest],
[Extent1].[field] AS [field]
FROM [dbo].[NonUnicodeTest] AS [Extent1]
WHERE ([Extent1].[field] = @p__linq__0) AND ([Extent1].[field] = @p__linq__1)’,N’@p__linq__0 varchar(8000),@p__linq__1 varchar(8000)’,@p__linq__0=’field1′,@p__linq__1=’field2′

 

Fijese que ya hemos puesto en negrita las diferencias, en la sentencia OR Entity Framework no es capaz  de reconocer el patrón de unicode/no unicode y aplicar el face correspondiente. Desde luego, esto es un bug en toda regla, seguramente porque la forma de analizar el CQT no es el mismo para la expresion OR que para la expresion AND.

 

Por suerte, disponemos de una forma de resolver este problema y es mediante una Model Defined Function. las mismas que vimos en un post anterior. De la misma forma que podemos crear nuevas funciones definidas en el modelo EF pone a nuestra disposición un conjunto de ellas out-of-box. Estas funciones las podemos encontrar dentro de las clases EntityFunctions y SqlFunctions, en concreto EntityFunctions nos provee de una función definida en el modelo con el nombre AsNonUnicode. Su utilización se muestra como se ve a continuación:

 

El resultado, despues del uso de EntityFunctions ya resulta el que esperamos:

 

exec sp_executesql N’SELECT
[Extent1].[idTest] AS [idTest],
[Extent1].[field] AS [field]
FROM [dbo].[NonUnicodeTest] AS [Extent1]
WHERE [Extent1].[field] IN (@p__linq__0,@p__linq__1)’,N’@p__linq__0 varchar(8000),@p__linq__1 varchar(8000)’,@p__linq__0=’field1′,@p__linq__1=’field2′

 

Saludos

Unai Zorrilla Castro