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

7 comentarios sobre “STE, las entidades duplicadas y el … vamos a ver!”

  1. Fantastico post Unai, gracias. Yo era una de esas personas que tenía este problema y no había profundizado en el porque. Yo lo solucione de una forma simple, no se sí es muy válida, en el diseñador renombre una de las propiedades duplicadas y ya no tuve este problema. En el objectStateManager ahora tengo 2 keys diferentes. En tu ejemplo sería llamarle a la propiedad country de order = countryOrder. ¿Crees que puede ser una altenativa? Gracias.

  2. Rafael, primero agradecerte los comentarios. Hombre, lo que comentas puede ser una solución “tecnica” al problema, pero el problema es que has tenido que hacer algo cuyo motivo quedará oculto para otra gente que puede ver/tocar/revisar ese código… Mi recomendacion siempre es tratar de mantener la consistencia del grafo, esta es nuestra obligacion, y despues usar foreign key properties. Para ello, puedes hacer métodos en tus entidades en plan SetCountry(int countryId).. para el caso de nuestro ejemplo….

    saludos
    Unai

  3. Gran post! En mi caso, de las soluciones que has dado, siempre he hecho hincapié en la a). Simplemente con un buen Equals en las STE:

    public override bool Equals(object obj)
    {
    MyClass aux = obj as MyClass;
    if (aux == null)
    return false;

    if(ID == 0)
    return Guid.Equals(aux.Guid);
    else
    return ID == aux.ID;
    }

    Estos problemas nos desaparecieron… La propiedad ID == 0 identifica entidades no persistidas, para las cuales se usa un Guid para identificarlas.

    Un saludo y gracias!

  4. @Rubén rucuerda que si sobreescribes el Equals DEBES sobreescribir el GetHashCode, quizás lo estés haciendo, pero es un error habitual no hacerlo, por eso me permito recordarlo.

    Un saludo.

  5. Hola Unai!

    Enhorabuena por el post! Era justamente lo que me estaba pasando a mí en un proyecto en el que estoy trabajando ahora mismo. En mi caso como comentas la consistencia del grafo es justamente el problema central. Yo utilizo STE con Silverlight y eso hace que en los ViewModels que tengo en mi aplicación. Utilizo las entidades directamente en el servicio y que a veces en cliente añada elementos que vienen de otros grafos consultados en el servidor. Es decir me puedo encontrar muy fácilmente con el escenario de que tengo más de una instancia de un objeto con la misma EntityKey. Esto es algo que se va a dar con mucha frecuencia porque justamente unas de las cosas que haces en cliente es mezclar ese grafo para mostrar los datos en UI.

    Por mi parte las cosas que he considerado oportuno para solventar ese problema son varios, uno es el patro visitor que Unai utiliza para buscar los elementos duplicados en el grafo y corregir las referencias duplicadas, yo tengo el mío propio hecho con reflexión. El otro mecanismo que utilizo para minimizar estos problemas es que cuando quiero actualizar una entidad en mi servicio lo que hago desde la aplicación de Silverlight es tener una lista de las propiedades complejas que sé que no voy a necesitar para la actualización que potencialmente tendrán objetos duplicados, con esta lista de propiedades prohibidas las pongo a null, me aseguro de que solo me llevo los datos que necesito para mi actualización. Una vez que el servicio me devuelve la nueva referencia actualizada vuelvo a conectar las propiedades eliminadas en la invocación.

    Las dos únicas pegas que yo le veo a STE son el problema de los duplicados, y el hecho de que cualquier objeto del que no lleve tracking te lo agrega con el estado de Added, que hacer que se añadan del nuevo los mismos elementos en la base de datos.

    Saludos. Luis.

  6. Unai, te cuento que estoy teniendo este problema pero en una propiedad de navegacion, es decir una coleccion. Explico la situacion: Cuando me llega del servidor un Customer que tiene una coleccion de Orders, a estas ultimas las presento en un ListView con Checkboxes guardando por cada registro el id del mismo. Ahora bien, el usuario en la UI marca los Orders que necesita que queden activos. Yo tendria que reconstruir la coleccion con los Orders y enviarla nuevamente al servidor para persistir el Customer con los Orders que se seleccionaron. Lo que yo intentaba hacer es un Customer.Orders.Clear() y despues hacia un New Order() por cada item seleccionado, y en cada uno ponia order.id = idcheckado para luego hacer un Customer.Orders.Add(order) y enviarlo al servidor. Esto por supuesto me esta queriendo agregar a la base de datos un order nuevo (con el mismo id) en vez de utilizar el que existe. La pregunta es como puedo aplicar lo que explicas en este post para una coleccion? Espero haber sido claro, sino intentare explicarlo mas detalladamente.

    Desde ya agradezco tu ayuda.

    Saludos.
    Diego

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *