Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 18 de abril de 2023
Entity Framework

Versión tras versión, Entity Framework Core sigue introduciendo novedades que lo hacen cada vez más potente y atractivo para los desarrolladores. Sin ir más lejos, EF7, la última entrega disponible, incorporó bastantes mejoras en términos de rendimiento, soporte para columnas JSON, mapeo TPC, mapeo a procedimientos almacenados y muchas otras.

Pero una de las novedades que me resultó más interesante, quizás por ser muy esperada, fue la posibilidad de realizar borrados y actualizaciones de forma masiva sin tener que recurrir a lanzar directamente comandos SQL. O dicho de otra forma, con EF7 podremos ejecutar sentencias DELETE y UPDATE sobre la base de datos sin abandonar LINQ :)

Borrado masivo de filas

Para eliminar filas de forma masiva y sin tener que cargar previamente las entidades en memoria, debemos usar ExecuteDelete() o su versión asíncrona ExecuteDeleteAsync(), ambos extensores de IQueryable.

El ejemplo de uso más sencillo que podríamos ver es el siguiente:

using var ctx = new FriendContext();
// Borramos todas las entidades Friend:
ctx.Friends.ExecuteDelete();
// O usando asincronía:
await ctx.Friends.ExecuteDeleteAsync();

Esto es totalmente equivalente a ejecutar un DELETE FROM Friends directamente sobre la base de datos.

Hay que tener en cuenta que este método ejecuta directamente la orden en la base de datos, pero no tiene nada que ver con el contenido actual de la caché de entidades en memoria (ChangeTracker), que no será consciente de los cambios realizados. Por tanto, el siguiente código fallaría en tiempo de ejecución:

// Cargamos un amigo en memoria
var firstFriend = ctx.Friends.First(); 

// Borramos todos los amigos
ctx.Friends.ExecuteDelete(); 

// Actualizamos una propiedad de la copia en memoria
firstFriend.Name = "Peter"; 

// Intentamos salvar los cambios
ctx.SaveChanges(); // Boom! DbUpdateConcurrencyException
                   // The database operation was expected to affect 1 row(s),
                   // but actually affected 0 row(s); 

Observad que en los ejemplos anteriores los métodos ExecuteDelete()/ExecuteDeleteAsync() los estamos ejecutando directamente contra el DbSet<Friend>, pero esto no tiene por qué ser necesariamente así. Ambos extensores están definidos en IQueryable, por lo que podemos aprovecharlo para introducir criterios de eliminación masivos.

Por ejemplo, en el siguiente código vemos cómo eliminar de la base de datos los amigos mayores de 30 años:

var count = await ctx.Friends.Where(f => f.Age > 30).ExecuteDeleteAsync();

Los criterios de búsqueda, como siempre, podrían contener cualquier tipo de expresión, por lo que podríamos introducir condiciones que utilicen navegación a distintos niveles de profundidad, que internamente darán lugar a joins entre distintas tablas. Por ejemplo:

await ctx.Friends.Where(f => f.State.Name == "New York").ExecuteDeleteAsync();

Esto sería traducido al motor como:

DELETE FROM [f]
      FROM [Friends] AS [f]
      INNER JOIN [States] AS [s] ON [f].[StateId] = [s].[Id]
      WHERE [s].[Name] = N'New York'

Actualización masiva de filas

De la misma forma, disponemos de los métodos ExecuteUpdate() y ExecuteUpdateAsync(). Estos reciben como parámetro un árbol de expresión que permite indicar las actualizaciones a realizar en cada entidad y, por cada una de ellas, el campo que debe ser actualizado y la expresión de actualización.

Veamos un primer ejemplo simple, que actualiza todos los amigos de la base de datos convirtiendo su nombre a mayúsculas:

await ctx.Friends.ExecuteUpdateAsync(
    setter => setter.SetProperty(f => f.Name, f => f.Name.ToUpper())
);

Como podemos observar, usamos SetProperty() para configurar la actualización del campo que le pasamos como primer parámetro, mientras que en el segundo especificamos la expresión que definirá su nuevo valor.

Por supuesto, podemos encadenar varias llamadas a SetProperty() para actualizar varios campos al mismo tiempo. En el siguiente código ampliamos el ejemplo anterior, incrementando el campo Age de cada entidad:

await ctx.Friends.ExecuteUpdateAsync(
    setter => setter.SetProperty(f => f.Name, f => f.Name.ToUpper())
                    .SetProperty(f=>f.Age, f=>f.Age+1)
);

Esto es lo que irá a la base de datos al ejecutarlo:

UPDATE [f]
SET [f].[Age] = [f].[Age] + 1,
    [f].[Name] = UPPER([f].[Name])
FROM [Friends] AS [f]

De nuevo, en los ejemplos anteriores podemos ver que estamos ejecutando ExecuteUpdateAsync() sobre el DbSet, pero dado que se trata de un extensor de IQueryable, podríamos introducir criterios de búsqueda de la misma forma que hacíamos con los métodos de eliminación:

await ctx.Friends
    .Where(f => f.State.Name == "New York")
    .ExecuteUpdateAsync(setter => setter.SetProperty(
        f => f.Name,
        f => f.Name + " from New York city!")
);

El SQL generado será algo como esto:

UPDATE [f]
    SET [f].[Name] = [f].[Name] + N' from New York city!'
    FROM [Friends] AS [f]
    INNER JOIN [States] AS [s] ON [f].[StateId] = [s].[Id]
    WHERE [s].[Name] = N'New York'

Ojo, como también ocurría con los borrados, estas actualizaciones van directamente contra la base de datos, por lo que el ChangeTracker no será consciente de los cambios realizados, como se puede comprobar a continuación:

var firstFriend = ctx.Friends.First();
Console.WriteLine(firstFriend.Name); // "John"

await ctx.Friends
    .ExecuteUpdateAsync(setter => setter.SetProperty(
        f => f.Name, 
        f => f.Name.ToUpper()
    ));

Console.WriteLine(firstFriend.Name); // "John"

Para finalizar, vale la pena añadir algunas observaciones que se indican en la documentación oficial.

En primer lugar, las operaciones de actualización y eliminación de filas se realizan en una única tabla. Aunque a priori parezca que no tiene importancia, sí puede llegar a ser un problema si estamos usando mapeos que impliquen el almacenamiento en tablas distintas, como puede ser TPT (Table per type).

También es importante tener en cuenta que las eliminaciones deben seguir las restricciones del modelo de datos, por lo que si intentamos eliminar entidades que tienen filas relacionadas en otras tablas, podría generase un error.

Por último, es bueno saber que las operaciones de actualización y borrado se ejecutan de forma independiente, no hay ningún tipo de transacción implícita. Si queremos que una llamada a ExecuteUpdate() o ExecuteDelete() se realice transaccionalmente con otras operaciones, será necesario crear manualmente la transacción de forma que envuelva a todas las acciones implicadas.

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!