Compartir a través de


Seguimiento explícito de entidades

Cada instancia de DbContext realiza un seguimiento de los cambios realizados en las entidades. Estas entidades de las que se realiza un seguimiento, a su vez, impulsan los cambios en la base de datos cuando se llama a SaveChanges.

El seguimiento de cambios de Entity Framework Core (EF Core) funciona mejor cuando se utiliza la misma instancia de DbContext para consultar las entidades y actualizarlas mediante una llamada a SaveChanges. Esto se debe a que EF Core realiza un seguimiento automático del estado de las entidades consultadas y, a continuación, detecta los cambios realizados en estas entidades cuando se llama a SaveChanges. Este enfoque se trata en Change Tracking en EF Core.

Sugerencia

En este documento se da por supuesto que se comprenden los estados de entidad y los conceptos básicos del seguimiento de cambios de EF Core. Consulte Change Tracking en EF Core para obtener más información sobre estos temas.

Sugerencia

Puede ejecutar y depurar en todo el código de este documento descargando el código de ejemplo de GitHub.

Sugerencia

Para simplificar, este documento utiliza métodos sincrónicos como SaveChanges, y hace referencia a ellos, en lugar de sus equivalentes asincrónicos como SaveChangesAsync. La llamada al método asincrónico y su espera pueden sustituirse a menos que se indique lo contrario.

Introducción

Las entidades se pueden "adjuntar" explícitamente a un DbContext de modo que el contexto realice un seguimiento de esas entidades. Esto es principalmente útil en los siguientes casos:

  1. Crear nuevas entidades que se insertarán en la base de datos.
  2. Volver a adjuntar entidades desconectadas consultadas previamente por una instancia de DbContext diferente.

La mayoría de las aplicaciones necesitarán la primera de ellas, y la gestionan principalmente los métodos DbContext.Add.

La segunda solo es necesaria para las aplicaciones que cambian las entidades o sus relaciones mientras no se realiza el seguimiento de las entidades. Por ejemplo, una aplicación web puede enviar entidades al cliente web donde el usuario realiza cambios y devuelve las entidades. Estas entidades se denominan "desconectadas", ya que originalmente se consultaron desde DbContext, pero luego se desconectaron de ese contexto cuando se enviaron al cliente.

La aplicación web ahora debe volver a adjuntar estas entidades para que se realice un seguimiento de ellas de nuevo e indicar los cambios realizados para que SaveChanges pueda realizar las actualizaciones adecuadas en la base de datos. Esto se controla principalmente por los métodos DbContext.Attach y DbContext.Update.

Sugerencia

Normalmente, no debería ser necesario adjuntar entidades a la misma instancia DbContext desde la que se consultaron. No realice rutinariamente una consulta sin seguimiento y luego adjunte las entidades devueltas al mismo contexto. Esto será más lento que usar una consulta de seguimiento, y también puede dar lugar a problemas como la falta de valores de propiedad reemplazada, lo que dificulta la obtención correcta.

Valores clave generados frente a explícitos

De forma predeterminada, las propiedades de clave entera y GUID están configuradas para usar valores de clave generados automáticamente. Esto tiene una gran ventaja para el seguimiento de cambios: un valor de clave sin establecer indica que la entidad es "nueva". Por "nueva" se entiende que aún no se han insertado en la base de datos.

En las siguientes secciones se usan dos modelos. El primero está configurado para no usar valores de clave generados:

public class Blog
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }
}

Los valores de clave no generados (es decir, establecidos explícitamente) se muestran primero en cada ejemplo porque todo es muy explícito y fácil de seguir. A continuación, se muestra un ejemplo en el que se usan valores de clave generados:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }
}

Tenga en cuenta que las propiedades de clave de este modelo no necesitan ninguna configuración adicional, ya que el uso de valores de clave generados es el valor predeterminado para las claves de enteras simples.

Inserción de nuevas entidades

Valores de clave explícitos

Se debe realizar un seguimiento de una entidad en el estado Added para ser insertada por SaveChanges. Normalmente, las entidades se colocan en el estado Added llamando a uno de DbContext.Add, DbContext.AddRange, DbContext.AddAsync, DbContext.AddRangeAsync o los métodos equivalentes en DbSet<TEntity>.

Sugerencia

Todos estos métodos funcionan de la misma manera en el contexto del seguimiento de cambios. Consulte Características adicionales de Change Tracking para obtener más información.

Por ejemplo, para iniciar el seguimiento de un nuevo blog:

context.Add(
    new Blog { Id = 1, Name = ".NET Blog", });

Al inspeccionar la vista de depuración de seguimiento de cambios después de esta llamada muestra que el contexto está realizando el seguimiento de la nueva entidad en el estado Added:

Blog {Id: 1} Added
  Id: 1 PK
  Name: '.NET Blog'
  Posts: []

Sin embargo, los métodos Add no solo funcionan en una entidad individual. En realidad, comienzan a realizar un seguimiento de un grafo de entidades relacionadas, poniéndolas a todas en el estado Added. Por ejemplo, para insertar un nuevo blog y nuevas publicaciones asociadas:

context.Add(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

El contexto ahora realiza el seguimiento de todas estas entidades como Added:

Blog {Id: 1} Added
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Added
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Added
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Observe que se han establecido valores explícitos para las propiedades de clave de Id en los ejemplos anteriores. Esto se debe a que el modelo se ha configurado para usar valores de clave establecidos explícitamente, en lugar de valores de clave generados automáticamente. Cuando no se usan claves generadas, las propiedades de clave deben establecerse explícitamente antes de llamar a Add. Estos valores de clave se insertan cuando se llama a SaveChanges. Por ejemplo, al usar SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Id", "Name")
VALUES (@p0, @p1);

-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String), @p3='1' (DbType = String), @p4='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p5='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p2, @p3, @p4, @p5);

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String), @p1='1' (DbType = String), @p2='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p3='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2, @p3);

Se realiza un seguimiento de todas estas entidades en el estado Unchanged después de completar SaveChanges, ya que estas entidades ahora existen en la base de datos:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Valores de clave generados

Como se mencionó anteriormente, las propiedades de clave entera y GUID están configuradas para usar valores de clave generados automáticamente de forma predeterminada. Esto significa que la aplicación no debe establecer ningún valor de clave explícitamente. Por ejemplo, para insertar un nuevo blog y publicaciones con valores de clave generados:

context.Add(
    new Blog
    {
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Al igual que en el caso de los valores de clave explícitos, el contexto ahora realiza el seguimiento de todas estas entidades como Added:

Blog {Id: -2147482644} Added
  Id: -2147482644 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -2147482637}, {Id: -2147482636}]
Post {Id: -2147482637} Added
  Id: -2147482637 PK Temporary
  BlogId: -2147482644 FK Temporary
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: -2147482644}
Post {Id: -2147482636} Added
  Id: -2147482636 PK Temporary
  BlogId: -2147482644 FK Temporary
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: -2147482644}

Observe en este caso que se han generado valores de clave temporal para cada entidad. EF Core usa estos valores hasta que se llama a SaveChanges, en cuyo punto se leen los valores de clave reales de la base de datos. Por ejemplo, al usar SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Name")
VALUES (@p0);
SELECT "Id"
FROM "Blogs"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p2='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p3='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p1, @p2, @p3);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Una vez completado SaveChanges, todas las entidades se han actualizado con sus valores de clave reales y se les realiza un seguimiento en el estado Unchanged, ya que ahora coinciden con el estado de la base de datos:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Este es exactamente el mismo estado final que el ejemplo anterior que usó valores de clave explícitos.

Sugerencia

Se puede establecer un valor de clave explícito incluso cuando se usan valores de clave generados. A continuación, EF Core intentará insertar con este valor de clave. Algunas configuraciones de base de datos, incluidas SQL Server con columnas de identidad, no admiten dichas inserciones y se tirarán (ver estos documentos para obtener una solución alternativa).

Adjuntar entidades existentes

Valores de clave explícitos

Se realiza un seguimiento de las entidades en el estado Unchanged devueltas por las consultas. El estado Unchanged significa que la entidad no se ha modificado desde que se ha consultado. Una entidad desconectada, quizás devuelta de un cliente web en una solicitud HTTP, se puede poner en este estado mediante DbContext.Attach, DbContext.AttachRange o los métodos equivalentes en DbSet<TEntity>. Por ejemplo, para iniciar el seguimiento de un blog existente:

context.Attach(
    new Blog { Id = 1, Name = ".NET Blog", });

Nota:

En los siguientes ejemplos se crean entidades explícitamente con new por motivos de simplicidad. Normalmente, las instancias de la entidad provendrán de otro origen, como por ejemplo, si se han descentralizado desde un cliente o si se han creado a partir de datos en una publicación HTTP.

La inspección de la vista de depuración del rastreador de cambios después de esta llamada muestra que se le hace un seguimiento a la entidad en el estado Unchanged:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: []

Al igual que Add, Attach establece un grafo completo de entidades conectadas al estado Unchanged. Por ejemplo, para adjuntar un blog existente y publicaciones existentes asociadas:

context.Attach(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

El contexto ahora realiza el seguimiento de todas estas entidades como Unchanged:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Llamar a SaveChanges en este momento no tendrá ningún efecto. Todas las entidades se marcan como Unchanged, por lo que no hay nada que actualizar en la base de datos.

Valores de clave generados

Como se mencionó anteriormente, las propiedades de clave entera y GUID están configuradas para usar valores de clave generados automáticamente de forma predeterminada. Esto tiene una gran ventaja al trabajar con entidades desconectadas: un valor de clave sin establecer indica que la entidad aún no se ha insertado en la base de datos. Esto permite al rastreador de cambios detectar automáticamente nuevas entidades y ponerlas en el estado Added. Por ejemplo, considere la posibilidad de adjuntar este grafo de un blog y sus publicaciones:

context.Attach(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            },
            new Post
            {
                Title = "Announcing .NET 5.0",
                Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
            },
        }
    });

El blog tiene un valor de clave de 1, lo que indica que ya existe en la base de datos. Dos de las publicaciones también tienen valores clave establecidos, pero el tercero no. EF Core verá este valor de clave como 0, el valor predeterminado de CLR para un entero. Esto da como resultado que EF Core marque la nueva entidad como Added en lugar de Unchanged:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482636}]
Post {Id: -2147482636} Added
  Id: -2147482636 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 includes many enhancements, including single file a...'
  Title: 'Announcing .NET 5.0'
  Blog: {Id: 1}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'

Llamar a SaveChanges en este momento no hace nada con las entidades Unchanged, pero inserta la nueva entidad en la base de datos. Por ejemplo, al usar SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Lo importante que hay que tener en cuenta aquí es que, con los valores de clave generados, EF Core puede distinguir automáticamente las entidades nuevas de las existentes en un grafo desconectado. En pocas palabras, cuando se usan claves generadas, EF Core siempre insertará una entidad cuando esa entidad no tenga ningún valor de clave establecido.

Actualización de entidades existentes

Valores de clave explícitos

DbContext.Update, DbContext.UpdateRangey los métodos equivalentes en DbSet<TEntity> se comportan exactamente como los métodos Attach descritos anteriormente, excepto que las entidades se colocan en el estado Modified en lugar del estado Unchanged. Por ejemplo, para iniciar el seguimiento de un blog existente como Modified:

context.Update(
    new Blog { Id = 1, Name = ".NET Blog", });

Al inspeccionar la vista de depuración de seguimiento de cambios después de esta llamada muestra que el contexto está realizando el seguimiento de esta entidad en el estado Modified:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: []

Al igual que con Add y Attach, Update marca un grafo completo de entidades relacionadas como Modified. Por ejemplo, para adjuntar un blog existente y publicaciones existentes asociadas como Modified:

context.Update(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

El contexto ahora realiza el seguimiento de todas estas entidades como Modified:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
  Title: 'Announcing the Release of EF Core 5.0' Modified
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
  Title: 'Announcing F# 5' Modified
  Blog: {Id: 1}

Al llamar a SaveChanges en este momento, las actualizaciones se enviarán a la base de datos para todas estas entidades. Por ejemplo, al usar SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

Valores de clave generados

Al igual que con Attach, los valores de clave generados tienen la misma ventaja principal para Update: un valor de clave sin establecer indica que la entidad es nueva y aún no se ha insertado en la base de datos. Al igual que con Attach, esto permite a DbContext detectar automáticamente nuevas entidades y ponerlas en el estado Added. Por ejemplo, considere la posibilidad de llamar a Update con este grafo de un blog y sus publicaciones:

context.Update(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            },
            new Post
            {
                Title = "Announcing .NET 5.0",
                Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
            },
        }
    });

Al igual que con el ejemplo de Attach, la publicación sin ningún valor de clave se detecta como nuevo y se establece en el estado Added. Las otras entidades se marcan como Modified:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482633}]
Post {Id: -2147482633} Added
  Id: -2147482633 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 includes many enhancements, including single file a...'
  Title: 'Announcing .NET 5.0'
  Blog: {Id: 1}
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
  Title: 'Announcing the Release of EF Core 5.0' Modified
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
  Title: 'Announcing F# 5' Modified
  Blog: {Id: 1}

Al llamar a SaveChanges en este momento, las actualizaciones se enviarán a la base de datos para todas las entidades existentes, mientras se inserta la nueva entidad. Por ejemplo, al usar SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Esta es una manera muy fácil de generar actualizaciones e inserciones desde un grafo desconectado. Sin embargo, da como resultado actualizaciones o inserciones que se envían a la base de datos para cada propiedad de cada entidad con seguimiento, incluso cuando es posible que algunos valores de propiedad no se hayan cambiado. No se preocupe; para muchas aplicaciones con grafos pequeños, esta puede ser una manera fácil y pragmática de generar actualizaciones. Dicho esto, otros patrones más complejos a veces pueden dar lugar a actualizaciones más eficaces, como se describe en Resolución de identidades en EF Core.

Eliminación de entidades existentes

Para que SaveChanges elimine una entidad, se debe realizar un seguimiento en el estado Deleted. Normalmente, las entidades se ponen en el estado de Deleted llamando a uno de DbContext.Remove, DbContext.RemoveRangeo los métodos equivalentes en DbSet<TEntity>. Por ejemplo, para marcar una publicación existente como Deleted:

context.Remove(
    new Post { Id = 2 });

Al inspeccionar la vista de depuración de seguimiento de cambios después de esta llamada muestra que el contexto está realizando el seguimiento de la entidad en el estado Deleted:

Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: <null> FK
  Content: <null>
  Title: <null>
  Blog: <null>

Esta entidad se eliminará cuando se llame a SaveChanges. Por ejemplo, al usar SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

Una vez completado SaveChanges, la entidad eliminada se desasocia de DbContext, ya que ya no existe en la base de datos. Por lo tanto, la vista de depuración está vacía porque no se realiza el seguimiento de ninguna entidad.

Eliminación de entidades dependientes o secundarias

La eliminación de entidades dependientes o secundarias de un grafo es más sencilla que eliminar entidades principales o primarias. Para obtener más información, consulte la siguiente sección y el artículo Cambio de las claves externas y las navegaciones.

Es inusual llamar a Remove en una entidad creada con new. Además, a diferencia de Add, Attach y Update, es poco habitual llamar a Remove en una entidad que no esté rastreada en el estado Unchanged o Modified. En su lugar, es habitual realizar un seguimiento de una sola entidad o grafo de entidades relacionadas y, a continuación, llamar a Remove en las entidades que se deben eliminar. Normalmente, este grafo de entidades con seguimiento se crea mediante:

  1. Ejecución de una consulta para las entidades
  2. Usar los métodos Attach o Update en un grafo de entidades desconectadas, como se describe en las secciones anteriores.

Por ejemplo, es más probable que el código de la sección anterior obtenga una publicación de un cliente y, a continuación, haga algo parecido a esto:

context.Attach(post);
context.Remove(post);

Esto se comporta exactamente del mismo modo que en el ejemplo anterior, ya que llamar a Remove en una entidad sin seguimiento hace que se adjunte primero y, a continuación, se marque como Deleted.

En ejemplos más realistas, primero se adjunta un grafo de entidades y luego algunas de esas entidades se marcan como eliminadas. Por ejemplo:

// Attach a blog and associated posts
context.Attach(blog);

// Mark one post as Deleted
context.Remove(blog.Posts[1]);

Todas las entidades se marcan como Unchanged, excepto aquella en la que se llamó a Remove:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Esta entidad se eliminará cuando se llame a SaveChanges. Por ejemplo, al usar SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

Una vez completado SaveChanges, la entidad eliminada se desasocia de DbContext, ya que ya no existe en la base de datos. Otras entidades permanecen en el estado Unchanged:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}

Eliminación de entidades principales o primarias

Cada relación que conecta dos tipos de entidad tiene un extremo principal o primario y un extremo dependiente o secundario. La entidad dependiente o secundaria es la que tiene la propiedad de clave externa. En una relación uno a varios, la entidad principal o primaria se encuentra en el lado "uno" y la secundaria o dependiente está en el lado "varios". Para obtener más información, consulte Relaciones.

En los ejemplos anteriores se estaba eliminando una publicación, que es una entidad dependiente o secundaria en la relación de uno a varios de publicaciones de un blog. Esto es relativamente sencillo, ya que la eliminación de una entidad dependiente o secundaria no tiene ningún impacto en otras entidades. Por otro lado, la eliminación de una entidad principal o primaria también debe afectar a las entidades dependientes o secundarias. Si no lo hace, dejaría un valor de clave externa que hace referencia a un valor de clave principal que ya no existe. Este estado del modelo no es válido y produce un error de restricción referencial en la mayoría de las bases de datos.

Este estado de modelo no válido se puede controlar de dos maneras:

  1. Establecer los valores FK en null. Esto indica que las dependientes o secundarias ya no están relacionadas con ninguna entidad principal o primaria. Este es el valor predeterminado para las relaciones opcionales en las que la clave externa debe admitir un valor null. Establecer el FK en null no es válido para las relaciones necesarias, donde la clave externa normalmente no acepta valores null.
  2. Eliminación de los elementos dependientes o secundarios. Este es el valor predeterminado para las relaciones necesarias y también es válido para las relaciones opcionales.

Consulte Cambio de claves externas y navegación para obtener información detallada sobre el seguimiento de cambios y las relaciones.

Relaciones opcionales

La propiedad de clave externa Post.BlogId admite valores null en el modelo que hemos usado. Esto significa que la relación es opcional y, por tanto, el comportamiento predeterminado de EF Core es establecer BlogId propiedades de clave externa en null cuando se elimina el blog. Por ejemplo:

// Attach a blog and associated posts
context.Attach(blog);

// Mark the blog as deleted
context.Remove(blog);

La inspección de la vista de depuración de seguimiento de cambios después de la llamada a Remove muestra que, según lo previsto, el blog ahora está marcado como Deleted:

Blog {Id: 1} Deleted
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: <null>
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>

Y lo que es más interesante, todas las publicaciones relacionadas ahora están marcadas como Modified. Esto se debe a que la propiedad de clave externa de cada entidad se ha establecido en null. Al llamar a SaveChanges, se actualiza el valor de clave externa de cada publicación a null en la base de datos, antes de eliminar el blog:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p2;
SELECT changes();

Una vez completado SaveChanges, la entidad eliminada se desasocia de DbContext, ya que ya no existe en la base de datos. Otras entidades ahora se marcan como Unchanged con valores de clave externa null, lo que coincide con el estado de la base de datos:

Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: <null> FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: <null>
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: <null> FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>

Relaciones obligatorias

Si la propiedad de clave externa Post.BlogId no acepta valores null, la relación entre blogs y publicaciones se convierte en "obligatoria". En esta situación, EF Core eliminará, de forma predeterminada, las entidades dependientes o secundarias cuando se elimine la primaria o principal. Por ejemplo, eliminar un blog con entradas relacionadas como en el ejemplo anterior:

// Attach a blog and associated posts
context.Attach(blog);

// Mark the blog as deleted
context.Remove(blog);

La inspección de la vista de depuración de seguimiento de cambios después de la llamada a Remove muestra que, según lo previsto, el blog vuelve a estar marcado como Deleted:

Blog {Id: 1} Deleted
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Deleted
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Lo más interesante en este caso es que todas las publicaciones relacionadas también se han marcado como Deleted. Llamar a SaveChanges hace que el blog y todas las entradas relacionadas se eliminen de la base de datos:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p1;

Una vez completado SaveChanges, todas las entidades eliminadas se desasocian de DbContext, ya que ya no existen en la base de datos. Por lo tanto, el resultado de la vista de depuración está vacío.

Nota:

Este documento solo araña la superficie del trabajo con relaciones en EF Core. Consulte Relaciones para obtener más información sobre el modelado de relaciones y Cambio de claves externas y navegaciones para obtener más información sobre cómo actualizar o eliminar entidades dependientes o secundarias al llamar a SaveChanges.

Seguimiento personalizado con TrackGraph

ChangeTracker.TrackGraph funciona como Add, Attach y Update, excepto que genera una devolución de llamada para cada instancia de entidad antes de realizar el seguimiento. Esto permite usar lógica personalizada al determinar cómo realizar un seguimiento de entidades individuales en un grafo.

Por ejemplo, considere la regla que EF Core usa al realizar el seguimiento de entidades con valores de clave generados: si el valor de clave es cero, la entidad es nueva y debe insertarse. Vamos a extender esta regla para decir que si el valor de clave es negativo, se debe eliminar la entidad. Esto nos permite cambiar los valores de clave principal de las entidades de un grafo desconectado para marcar las entidades eliminadas:

blog.Posts.Add(
    new Post
    {
        Title = "Announcing .NET 5.0",
        Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
    }
);

var toDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
toDelete.Id = -toDelete.Id;

A continuación, se puede realizar un seguimiento de este grafo desconectado mediante TrackGraph:

public static void UpdateBlog(Blog blog)
{
    using var context = new BlogsContext();

    context.ChangeTracker.TrackGraph(
        blog, node =>
        {
            var propertyEntry = node.Entry.Property("Id");
            var keyValue = (int)propertyEntry.CurrentValue;

            if (keyValue == 0)
            {
                node.Entry.State = EntityState.Added;
            }
            else if (keyValue < 0)
            {
                propertyEntry.CurrentValue = -keyValue;
                node.Entry.State = EntityState.Deleted;
            }
            else
            {
                node.Entry.State = EntityState.Modified;
            }

            Console.WriteLine($"Tracking {node.Entry.Metadata.DisplayName()} with key value {keyValue} as {node.Entry.State}");
        });

    context.SaveChanges();
}

Para cada entidad del grafo, el código anterior comprueba el valor de la clave principal antes de realizar el seguimiento de la entidad. En el caso de los valores de clave sin establecer (cero), el código hace lo que EF Core haría normalmente. Es decir, si no se establece la clave, la entidad se marca como Added. Si se establece la clave y el valor no es negativo, la entidad se marca como Modified. Sin embargo, si se encuentra un valor de clave negativo, se restaura su valor real no negativo y se realiza el seguimiento de la entidad como Deleted.

El resultado de la ejecución de este código es:

Tracking Blog with key value 1 as Modified
Tracking Post with key value 1 as Modified
Tracking Post with key value -2 as Deleted
Tracking Post with key value 0 as Added

Nota:

Para simplificar, este código supone que cada entidad tiene una propiedad de clave principal entera denominada Id. Esto se podría codificar en una interfaz o clase base abstracta. Como alternativa, la propiedad o las propiedades de la clave principal se podrían obtener de los metadatos IEntityType, de modo que este código funcionaría con cualquier tipo de entidad.

TrackGraph tiene dos sobrecargas. En la sobrecarga simple usada anteriormente, EF Core determina cuándo detener el recorrido del grafo. En concreto, deja de visitar nuevas entidades relacionadas desde una entidad determinada cuando ya se realiza un seguimiento de esa entidad o cuando la devolución de llamada no inicia el seguimiento de la entidad.

La sobrecarga avanzada, ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>), tiene una devolución de llamada que devuelve un bool. Si la devolución de llamada devuelve false, el recorrido del grafo se detiene; de lo contrario, continúa. Se debe tener cuidado para evitar bucles infinitos al usar esta sobrecarga.

La sobrecarga avanzada también permite proporcionar el estado a TrackGraph y este estado se pasa a cada devolución de llamada.