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:
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:
|
<span class="kwrd">private</span> <span class="kwrd">static</span> <span class="kwrd">void</span> Save(Customer customerInClient) { <span class="kwrd">using</span> (UnitOfWork unitOfWork = <span class="kwrd">new</span> UnitOfWork()) { unitOfWork.Customer.ApplyChanges(customerInClient); unitOfWork.SaveChanges(); } } |
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:
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:
|
<span class="kwrd">static</span> <span class="kwrd">void</span> Plan2() { Customer customerInClient = GetCustomerFromServer(); Country countryInClient = GetCountryFromServer(); Order order = <span class="kwrd">new</span> Order() { Total = 12M, Country = countryInClient }; customerInClient.Orders.Add(order); Save(customerInClient); } |
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:
|
<span class="kwrd">public</span> <span class="kwrd">static</span> TEntity MergeWith<TEntity, TGraph>(<span class="kwrd">this</span> TEntity entity, TGraph graph, Func<TEntity, TEntity, <span class="kwrd">bool</span>> keyComparer) <span class="kwrd">where</span> TEntity : <span class="kwrd">class</span>,IObjectWithChangeTracker <span class="kwrd">where</span> TGraph : <span class="kwrd">class</span>,IObjectWithChangeTracker { <span class="kwrd">using</span> (ChangeTrackerIterator iterator = ChangeTrackerIterator.Create(graph)) { <span class="kwrd">return</span> iterator.OfType<TEntity>().SingleOrDefault(e => keyComparer(entity, e)) ?? entity; } } |
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:
|
<span class="kwrd">static</span> <span class="kwrd">void</span> Plan3() { Customer customerInClient = GetCustomerFromServer(); Country countryInClient = GetCountryFromServer(); Order order = <span class="kwrd">new</span> Order() { Total = 12M, Country = countryInClient.MergeWith(customerInClient,(c1,c2)=>c1.IdCountry == c2.IdCountry) }; customerInClient.Orders.Add(order); Save(customerInClient); } |
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.
|
<span class="kwrd">static</span> <span class="kwrd">void</span> Plan4() { Customer customerInClient = GetCustomerFromServer(); Country countryInClient = GetCountryFromServer(); Order order = <span class="kwrd">new</span> Order() { Total = 12M, IdCountry = 1 }; customerInClient.Orders.Add(order); Save(customerInClient); } |
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