jueves, 29 de enero de 2009

ADO.NET Entity Framework: ¿Cómo actualizar un objeto desconectado?

Un escenario común es el siguiente:

"Al enviar un formulario web, creo una entidad Pedido con los datos proporcionados por el usuario y lo entrego a mi capa de negocios o servicios para que se valide y persista en el almacén de datos."

En este escenario la entidad Pedido enviada desde la página web (o enviada desde un Web Service), se encuentra en estado Deattached. Para que podamos persistir sus cambios necesitamos asociarla a un contexto y luego llamar al método SaveChanges().

Si la entidad aún no existe en el almacén de datos, debemos agregarla. Esto es muy simple, usando los métodos AddTo* generados en el modelo:

context.AddToOrders(order);
context.SaveChanges();

En cambio si la entidad ya existe en el almacén de datos, entonces debemos asociarla al contexto y guardar los cambios. Esta tarea parece ser simple y podríamos intentar resolverla de la siguiente manera:

context.AttachTo("Orders", order);
context.SaveChanges();

Sin embargo, esto no funciona, pues el objeto order se asocia exitosamente al contexto pero en el estado Unchanged. Luego el método SaveChanges() no persiste ningún cambio.

La forma de resolver este problema es recuperando la entidad original desde el almacén de datos, aplicar los cambios, y volver a guardarla.

Buscando en la web encontré diferentes enfoques para resolver este problema. Muchos utilizan Reflection para recorrer todas las propiedades y asignar las que han cambiado. La que más me ha interesado la encontré en el blog de Cesar de la Torre.

Se trata de un extension method llamado AttachUpdated() aplicado al ObjectContext. Me he tomado el atrevimiento de renombrar le método AttacheUpdated() por UpdateObject(), pues me pareció más consistente con otros métodos que ya existen en el ObjectContext como AddObject() y DeleteObject(). El código es el siguiente:

public static void UpdateObject(this ObjectContext context, string entitySetName, EntityObject entity)
{
    var key = context.CreateEntityKey(entitySetName, entity);

    object originalEntity;
    if (context.TryGetObjectByKey(key, out originalEntity))
    {
        context.ApplyPropertyChanges(entitySetName, entity);
        context.ApplyReferencePropertyChanges(entity, (IEntityWithRelationships)originalEntity);
    }
    else
    {
        throw new ObjectNotFoundException();
    }
}

Este método primero crea la EntityKey asociada al objeto usando el método CreateEntityKey() y luego intenta recuperar el objeto desde el almacén de datos usando el método TryGetObjectByKey(). A continuación aplica los cambios usando el método ApplyPropertyChanges() y luego llama al método ApplyReferencePropertyChanges() que se encarga de aplicar los cambios a las entidades relacionadas. Este último método se trata de otro extension method:

private static void ApplyReferencePropertyChanges(this ObjectContext context, IEntityWithRelationships newEntity, IEntityWithRelationships oldEntity)
{
    foreach (var relatedEnd in oldEntity.RelationshipManager.GetAllRelatedEnds())
    {
        var oldRef = relatedEnd as EntityReference;

        if (oldRef != null)
        {
            var newRef = newEntity.RelationshipManager.GetRelatedEnd(oldRef.RelationshipName, oldRef.TargetRoleName) as EntityReference;
            if (newRef != null) oldRef.EntityKey = newRef.EntityKey;
        }
    }
}

Aquí no solo se resuelve el problema de aplicar los cambios a las propiedades del objeto en cuestión sino también los cambios en sus objetos relacionados.

Luego de crear estos extensions methods, el código final para actualizar una entidad desconectada quedaría como:

context.UpdateObject("Orders", order); //Extension method
context.SaveChanges();

He probado esto y funcionó para mí. Espero que también les sea de utilidad.

Saludos, Gus

3 comentarios:

Virginio dijo...

Excelente Gustavo,

Estuve recorriendo el codigo de Cesar de la Torre y no me funcionaba, hasta que le encontre la vuelta, ahora me encuentro con tu codigo y comparando con el de Cesar veo que falta una linea, el que esta resaltado.

public static void UpdateObject(this ObjectContext context, string entitySetName, EntityObject entity)
{
var key = context.CreateEntityKey(entitySetName, entity);

object originalEntity;
if (context.TryGetObjectByKey(key, out originalEntity))
{
context.ApplyPropertyChanges(entitySetName, entity);
context.ApplyReferencePropertyChanges(entity, (IEntityWithRelationships)originalEntity);
}
else
{
throw new ObjectNotFoundException();
}
}

Saludos

Gustavo Azcona dijo...

Gracias Virginio por tus comentarios.
Justamente al tratarse de una entidad desconectada, normalmente, el EntityKey no está establecido y el método TryGetObjectByKey() falla. Al usar el método CreateEntityKey() nos aseguramos de crear la EntityKey que faltaba.

Si tienes algún truco o conocimiento que desees compartir, no dudes en postearlo o envíamelo por correo a gustavoazcona@hotmail.com que me encargaré de publicarlo con los créditos correspondientes. Saludos.

Anónimo dijo...

Hola¡ Soy nuevo en esto del EntityFramework. Como se puede crear una nueva EntityKey cuya clave sea compuesta? Por ejemplo sería el caso de una LineaFactura no? Esto lo pregunto porque tengo un DAO genérico donde tengo las operaciones básicas y además un método CreateEntityKey. O sería mejor implementarlo en el DAO de cada entidad según la necesidad?

Espero que me hallais entendido. Por cierto, muy buen post. Un saludo. Gracias de antemano.