ExecuteUpdate y ExecuteDelete
Nota:
Esta característica se incluyó por primera vez en EF Core 7.0.
ExecuteUpdate y ExecuteDelete son una manera de guardar datos en la base de datos sin usar el método SaveChanges() y el seguimiento de cambios tradicional de EF. Para una comparación introductoria de estas dos técnicas, consulte la página de información general sobre cómo guardar datos.
ExecuteDelete
Supongamos que necesita eliminar todos los blogs con una clasificación por debajo de un umbral determinado. El enfoque SaveChanges() tradicional requiere que haga lo siguiente:
foreach (var blog in context.Blogs.Where(b => b.Rating < 3))
{
context.Blogs.Remove(blog);
}
context.SaveChanges();
Esta es una manera bastante ineficaz de realizar esta tarea: consultamos en la base de datos todos los blogs que coinciden con nuestro filtro y, a continuación, consultamos, materializamos y realizamos un seguimiento de todas esas instancias; el número de entidades coincidentes podría ser enorme. A continuación, le indicamos al control de cambios de EF los blogs que se deben quitar y aplicaremos esos cambios mediante una llamada a SaveChanges(), que genera una instrucción DELETE
para cada uno de ellos.
Esta es la misma tarea realizada a través de la API ExecuteDelete:
context.Blogs.Where(b => b.Rating < 3).ExecuteDelete();
Usa los operadores LINQ conocidos para determinar a qué blogs afectará (como si se estuvieran consultando) y, a continuación, indica a EF que ejecute una instrucción SQL DELETE
en la base de datos:
DELETE FROM [b]
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Además de ser más fácil y rápido, se ejecuta de forma muy eficaz en la base de datos, sin cargar datos de la base de datos ni el control de cambios de EF. Tenga en cuenta que puede usar operadores LINQ arbitrarios para seleccionar los blogs que desea eliminar; estos se traducen a SQL para su ejecución en la base de datos, como si estuviera consultando esos blogs.
ExecuteUpdate
En lugar de eliminar estos blogs, ¿qué ocurre si, en cambio, queremos cambiar una propiedad para indicar que debe estar oculta? ExecuteUpdate es una manera similar de expresar una instrucción SQL UPDATE
:
context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdate(setters => setters.SetProperty(b => b.IsVisible, false));
Al igual que con ExecuteDelete
, primero usamos LINQ para determinar a qué blogs afectará; pero con ExecuteUpdate
también debemos expresar el cambio que se va a aplicar a los blogs coincidentes. Esto se hace llamando a SetProperty
dentro de la llamada ExecuteUpdate
y proporcionándole dos argumentos: la propiedad que se va a cambiar (IsVisible
) y el nuevo valor que debe tener (false
). Esto hace que se ejecute el siguiente código SQL:
UPDATE [b]
SET [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Actualización de varias propiedades
ExecuteUpdate
permite actualizar varias propiedades en una sola invocación. Por ejemplo, para establecer IsVisible
en false y Rating
, en cero, simplemente encadene llamadas adicionales a SetProperty
:
context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdate(setters => setters
.SetProperty(b => b.IsVisible, false)
.SetProperty(b => b.Rating, 0));
Esto ejecuta el siguiente código SQL:
UPDATE [b]
SET [b].[Rating] = 0,
[b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Referencia al valor de propiedad existente
En los ejemplos anteriores se actualizó la propiedad a un nuevo valor constante. ExecuteUpdate
también permite hacer referencia al valor de propiedad existente al calcular el nuevo; por ejemplo, para aumentar la clasificación de todos los blogs coincidentes en un valor, use lo siguiente:
context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdate(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));
Tenga en cuenta que el segundo argumento de SetProperty
es ahora una función lambda y no una constante como antes. Su parámetro b
representa el Blog que se está actualizando; dentro de esa lambda, por lo tanto, b.Rating
contiene la calificación antes de que ocurra cualquier cambio. Esto ejecuta el siguiente código SQL:
UPDATE [b]
SET [b].[Rating] = [b].[Rating] + 1
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Navegaciones y entidades relacionadas
ExecuteUpdate
no admite actualmente la referencia a las navegaciones dentro de la expresión lambda SetProperty
. Por ejemplo, supongamos que queremos actualizar todas las clasificaciones de los blogs para que la nueva sea la media de todas las clasificaciones de sus publicaciones. Podríamos intentemos usar ExecuteUpdate
de la siguiente manera:
context.Blogs.ExecuteUpdate(
setters => setters.SetProperty(b => b.Rating, b => b.Posts.Average(p => p.Rating)));
Sin embargo, EF permite realizar esta operación usando Select
primero para calcular la clasificación media y proyectarla en un tipo anónimo y, a continuación, usar ExecuteUpdate
:
context.Blogs
.Select(b => new { Blog = b, NewRating = b.Posts.Average(p => p.Rating) })
.ExecuteUpdate(setters => setters.SetProperty(b => b.Blog.Rating, b => b.NewRating));
Esto ejecuta el siguiente código SQL:
UPDATE [b]
SET [b].[Rating] = CAST((
SELECT AVG(CAST([p].[Rating] AS float))
FROM [Post] AS [p]
WHERE [b].[Id] = [p].[BlogId]) AS int)
FROM [Blogs] AS [b]
Seguimiento de cambios
Los usuarios familiarizados con SaveChanges
están acostumbrados a realizar varios cambios y a llamar a SaveChanges
para aplicarlos todos a la base de datos; el control de cambios de EF, que acumula o realiza un seguimiento de estos cambios, lo hace posible.
ExecuteUpdate
y ExecuteDelete
funcionan de forma muy diferente: surten efecto inmediatamente, en el momento en que se invocan. Esto significa que, aunque una sola operación ExecuteUpdate
o ExecuteDelete
puede afectar a muchas filas, no es posible acumular varias operaciones de este tipo y aplicarlas a la vez, por ejemplo, al llamar a SaveChanges
. De hecho, las funciones no son completamente conscientes del control de cambios de EF y no tienen ninguna interacción con él. Esto tiene consecuencias importantes.
Observe el código siguiente:
// 1. Query the blog with the name `SomeBlog`. Since EF queries are tracking by default, the Blog is now tracked by EF's change tracker.
var blog = context.Blogs.Single(b => b.Name == "SomeBlog");
// 2. Increase the rating of all blogs in the database by one. This executes immediately.
context.Blogs.ExecuteUpdate(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));
// 3. Increase the rating of `SomeBlog` by two. This modifies the .NET `Rating` property and is not yet persisted to the database.
blog.Rating += 2;
// 4. Persist tracked changes to the database.
context.SaveChanges();
Fundamentalmente, cuando se invoca a ExecuteUpdate
y todos los blogs se actualizan en la base de datos, el control de cambios de EF no se actualiza y la instancia de .NET con seguimiento sigue teniendo su valor de clasificación original, desde el punto en el que se consultó. Supongamos que la clasificación del blog era originalmente 5; una vez ejecutada la tercera línea, la clasificación de la base de datos es 6 (debido a ExecuteUpdate
), mientras que la clasificación en la instancia de .NET con seguimiento es 7. Cuando se llama a SaveChanges
, EF detecta que el nuevo valor 7 es diferente del valor original 5 y conserva ese cambio. El cambio realizado por ExecuteUpdate
se sobrescribe y no se tiene en cuenta.
Como resultado, suele ser una buena idea evitar mezclar modificaciones con SaveChanges
con seguimiento y sin él con ExecuteUpdate
/ExecuteDelete
.
Transacciones
Siguiendo con lo anterior, es importante comprender que ExecuteUpdate
y ExecuteDelete
no inician implícitamente una transacción cuando se invocan. Observe el código siguiente:
context.Blogs.ExecuteUpdate(/* some update */);
context.Blogs.ExecuteUpdate(/* another update */);
var blog = context.Blogs.Single(b => b.Name == "SomeBlog");
blog.Rating += 2;
context.SaveChanges();
Cada llamada a ExecuteUpdate
hace que se envíe una única instrucción SQL UPDATE
a la base de datos. Dado que no se crea ninguna transacción, si algún tipo de error impide que la segunda instrucción ExecuteUpdate
se complete correctamente, los efectos de la primera se conservan en la base de datos. De hecho, las cuatro operaciones anteriores: dos invocaciones de ExecuteUpdate
, una consulta y SaveChanges
, se ejecutan dentro de su propia transacción. Para ajustar varias operaciones en una sola transacción, inicie explícitamente una transacción con DatabaseFacade:
using (var transaction = context.Database.BeginTransaction())
{
context.Blogs.ExecuteUpdate(/* some update */);
context.Blogs.ExecuteUpdate(/* another update */);
...
}
Para más información sobre el control de transacciones, consulte Uso de transacciones.
Control de simultaneidad y filas afectadas
SaveChanges
proporciona control de simultaneidad automático mediante un token de simultaneidad para garantizar que una fila no haya cambiado entre el momento en que se cargara y cuando se guardaran cambios en ella. Puesto que ExecuteUpdate
y ExecuteDelete
no interactúan con el control de cambios, no aplican automáticamente el control de simultaneidad.
Sin embargo, ambos métodos devuelven el número de filas afectadas por la operación. Esto puede resultar especialmente útil si quiere implementar el control de simultaneidad usted mismo:
// (load the ID and concurrency token for a Blog in the database)
var numUpdated = context.Blogs
.Where(b => b.Id == id && b.ConcurrencyToken == concurrencyToken)
.ExecuteUpdate(/* ... */);
if (numUpdated == 0)
{
throw new Exception("Update failed!");
}
En este código usamos un operador LINQ Where
para aplicar una actualización a un blog específico y solo si su token de simultaneidad tiene un valor específico (por ejemplo, el que vimos al consultar el blog de la base de datos). Comprobamos cuántas filas actualizó ExecuteUpdate
realmente; si el resultado es cero, no se actualizaron filas y es probable que el token de simultaneidad se cambiara como resultado de una actualización simultánea.
Limitaciones
- Actualmente solo se admiten la actualización y la eliminación; la inserción debe realizarse con DbSet<TEntity>.Add y SaveChanges().
- Aunque las instrucciones SQL UPDATE y DELETE permiten recuperar los valores de columna originales de las filas afectadas, actualmente no es compatible con
ExecuteUpdate
yExecuteDelete
. - No se pueden procesar por lotes varias invocaciones de estos métodos. Cada invocación realiza su propio recorrido de ida y vuelta a la base de datos.
- Normalmente, con UPDATE o DELETE las bases de datos solo permiten modificar una sola tabla.
- Actualmente, estos métodos solo funcionan con proveedores de bases de datos relacionales.
Recursos adicionales
- Reunión de la comunidad de acceso a los datos de .NET donde hablaremos de
ExecuteUpdate
yExecuteDelete
.