Compartir a través de


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

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 y ExecuteDelete.
  • 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