martes, 8 de abril de 2014

Eliminar y actualizar eficientemente con Entity Framework

Después de llevar un tiempo programando con EF, me gustaría compartir un par de trucos para intentar evitar round-trips innecesarios al servidor, y de paso optimizar en la medida de lo posible el impacto de las instrucciones UPDATE y DELETE en nuestra base de datos.

Para el post utilizaré una base de datos con una sola tabla llamada Clientes.

clip_image001[8]

Imaginemos ahora unos típicos métodos para actualizar el nombre de un cliente y eliminar el registro.

static void EliminarCliente(int idCliente)

{

    using (var context = new GestionEntities())

    {

        var cliente = context.Clientes.Find(idCliente);

        context.Clientes.Remove(cliente);

        context.SaveChanges();

    }

}

 

static void ActualizarNombreCliente(int idCliente, string nombre)

{

    using (var context = new GestionEntities())

    {

        var cliente = context.Clientes.Find(idCliente);

        cliente.Nombre = nombre;

        context.SaveChanges();

    }           

}

Aunque pueden parecer métodos válidos (de hecho lo son), tiene algún problema de ineficiencia que podríamos paliar reescribiendo un poco el código y sobre todo, conociendo como funciona EF.

El motivo por el que podría acusarse el anterior código como ineficiente es que, tanto para actualizar como para eliminar, primero se está realizando un acceso a datos para cargar la entidad completa (el método Find). Cierto es que Find nos devolvería la entidad desde el contexto si ya hubiera sido previamente cargada, pero para este caso (donde se crea y destruye el contexto en cada método) y para el propósito del post, pensaremos que Find accede a datos porque no encuentra la entidad en memoria.

Un punto a favor de EF y que no quisiera dejar de señalar es que la instrucción de actualización sólo incluye los campos que hemos modificado, es decir, ActualizarNombreCliente tira la siguiente sentencia SQL:

exec sp_executesql N'UPDATE [dbo].[Clientes]

SET [Nombre] = @0

WHERE ([IdCliente] = @1)

',N'@0 nvarchar(50),@1 int',@0=N'Sergio',@1=1

 

Sin embargo, como nosotros queremos que nuestra actualización y eliminación sean lo más óptimas posibles, terminaríamos escribiendo este otro código:

static void EliminarCliente(int idCliente)

{

    var cliente = new Cliente { IdCliente = idCliente };

    using (var context = new GestionEntities())

    {

        context.Clientes.Attach(cliente);

        context.Clientes.Remove(cliente);

        context.SaveChanges();

    }

}

 

static void ActualizarNombreCliente(int idCliente, string nombre)

{

    var cliente = new Cliente { IdCliente = idCliente };

    using (var context = new GestionEntities())

    {

        context.Clientes.Attach(cliente);

        cliente.Nombre = nombre;

        context.SaveChanges();

    }

}

Con este código ahora sólo ejecutamos 2 sentencias en su más mínima expresión y sin previo acceso a datos para cargar la entidad en memoria.

exec sp_executesql N'UPDATE [dbo].[Clientes]

SET [Nombre] = @0

WHERE ([IdCliente] = @1)

',N'@0 nvarchar(50),@1 int',@0=N'Sergio',@1=1

 

exec sp_executesql N'DELETE [dbo].[Clientes]

WHERE ([IdCliente] = @0)',N'@0 int',@0=1

 

De DELETE no hay mucho más que hablar, sólo la precaución (también aplicable a UPDATE) de incluir en nuestra entidad todas las propiedades que son clave principal (Entity Key) o que están incluidas como parte de la concurrencia pesimista (Concurrency Mode = Fixed).

Sin embargo para UPDATE sí hay que comentar algo más al respecto. Si en nuestra tabla Clientes hubiera algún campo que no admitiera nulos y que no fuera parte de nuestra entidad (por ejemplo Descripcion o Pais fueran no nulables), la llamada a SaveChanges fallaría por la validación de EF y lanzaría un error del tipo System.Data.Entity.Validation.DbEntityValidationException.

No sé si habrá muchas más formas de solucionar esto, pero por mi parte yo lo hago deshabilitando la validación antes de la llamada a SaveChanges.

Claro está que si el contexto no es de usar y tirar (que será lo más normal), deberíamos después restaurar el valor de la propiedad ValidationOnSaveEnabled a su valor original.

Dicho esto, el código de actualizar quedaría así:

static void ActualizarNombreCliente(int idCliente, string nombre)

{

    var cliente = new Cliente { IdCliente = idCliente };

    using (var context = new GestionEntities())

    {

        context.Clientes.Attach(cliente);

        cliente.Nombre = nombre;

        context.Configuration.ValidateOnSaveEnabled = false;

        context.SaveChanges();

    }

}

Un saludo!

8 comentarios:

  1. Buenas,

    Muy buena tu comentario, quisiera hacerte una pregunta que va relacionado con optimizar la ejecusion del codigo. Supongamos que tengo un modelo con dos tablas relacionadas mediante llave Foranea, la momento de insertar un nuevo registro en una tabla EF intentaria insertar en ambas tabla debido a la asociacion que existe. Mi Pregunta es: Existe una forma de indicarle al EF que solo afecte a una sola entidad al momento de hacer Insert, Update o Delete?

    ResponderEliminar
  2. consulta, como el eliminar no tengo problema porque se elimina de la memoria el registro, pero cuando deseo actualizar un registro, todo funciona, pero al consultar el registro actualizado sigue mostrando e antiguo valor, lo que tengo que hacer es cerrar la app y volver a levantarla para visualizar los cambio, aunque en la bd si genera la actualizacion de inmediato.
    static void ActualizarNombreCliente(int idCliente, string nombre)
    {
    var cliente = new Cliente { IdCliente = idCliente };
    using (var context = new GestionEntities())
    {
    context.Clientes.Attach(cliente);
    cliente.Nombre = nombre;
    context.SaveChanges();
    }
    }
    que le estaria faltando a esto para que realice los cambio en tiempo real

    ResponderEliminar
  3. anónimo yo responde tu pregunta, tenes que fijarte bien cuando declares llaves de otra tabla en la tabla trabajada.

    public virtual Cliente cliente{set;get;}
    esto hara que tu tabla actual cambie el valor y la que este asociada
    el codigo correcto seria
    public virtual Cliente cliente{set;get;}
    public int ClienteID {set;get;}

    si te fijas en tu bd cambiaría la relación, y tus modificaciones solo afectaran a la que invocas, y las tablas que esten asociada por la llave verán el cambio de esa tabla.
    espero que te sirva.
    saludos.


    ResponderEliminar
  4. gracias ya lo resolvi, el problema era que tenia que reiniciar el db context

    ResponderEliminar