Явное отслеживание сущностей

Каждый экземпляр DbContext отслеживает изменения, внесенные в сущности. Эти отслеживаемые сущности, в свою очередь, записывают изменения в базу данных при вызове SaveChanges.

Отслеживание изменений Entity Framework Core (EF Core) лучше всего работает, если один и тот же DbContext экземпляр используется для запроса сущностей и обновления их путем вызова SaveChanges. Это происходит потому, что EF Core автоматически отслеживает состояние запрашиваемых сущностей и определяет изменения, внесенные в эти сущности при вызове SaveChanges. Этот подход рассматривается в Отслеживание изменений в EF Core.

Совет

В этом документе предполагается, что состояния сущности и основы отслеживания изменений EF Core понятны. Дополнительные сведения об этих разделах см. в Отслеживание изменений в EF Core.

Совет

Вы можете запустить и отладить весь код, используемый в этой документации, скачав пример кода из GitHub.

Совет

Для простоты в этой документации используются и описываются синхронные методы, такие как SaveChanges, а не их асинхронные эквиваленты, такие как SaveChangesAsync. Вызов и ожидание асинхронного метода можно заменить, если не указано иное.

Введение

Сущности могут быть явно присоединены к DbContext такому объекту, что контекст затем отслеживает эти сущности. Это в первую очередь полезно, когда:

  1. Создание новых сущностей, которые будут вставлены в базу данных.
  2. Повторное присоединение отключенных сущностей, которые ранее запрашивались другим экземпляром DbContext.

Первое из них потребуется большинству приложений и в первую очередь обрабатывается методами DbContext.Add .

Второй требуется только приложениям, которые изменяют сущности или их связи, пока сущности не отслеживаются. Например, веб-приложение может отправлять сущности в веб-клиент, где пользователь вносит изменения и отправляет сущности обратно. Эти сущности называются "отключенными", так как изначально они были запрошены из DbContext, но затем были отключены от этого контекста при отправке клиенту.

Теперь веб-приложение должно повторно подключить эти сущности, чтобы они снова отслеживались и указывают изменения, внесенные таким образом, чтобы SaveChanges внести соответствующие обновления в базу данных. Это в первую очередь обрабатывается методами и DbContext.Update методамиDbContext.Attach.

Совет

Присоединение сущностей к тому же экземпляру DbContext, из которых они запрашивались, обычно не должны быть необходимы. Не выполняйте обычно запрос без отслеживания, а затем присоединяйте возвращаемые сущности к тому же контексту. Это будет медленнее, чем при использовании запроса отслеживания, а также может привести к проблемам, таким как отсутствие значений теневых свойств, что затрудняет получение прав.

Созданные и явные значения ключей

По умолчанию свойства целочисленных и ключей GUID настраиваются для использования автоматически созданных значений ключей. Это имеет основное преимущество для отслеживания изменений: значение ключа без набора указывает, что сущность "новая". По "new" мы означают, что он еще не вставлен в базу данных.

Две модели используются в следующих разделах. Первая настроена не использовать созданные значения ключей:

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; }
}

Негенерированные (т. е. явно заданные) значения ключей отображаются сначала в каждом примере, так как все очень явно и легко следовать. Затем за этим следует пример, в котором используются созданные значения ключей:

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; }
}

Обратите внимание, что свойства ключей в этой модели не требуют дополнительной настройки, так как использование созданных значений ключей является значением по умолчанию для простых целых ключей.

Вставка новых сущностей

Явные значения ключей

Сущность должна быть отслеживаема в Added состоянии для вставки SaveChanges. Сущности обычно помещаются в добавленное состояние путем вызова одного из DbContext.Addметодов , DbContext.AddAsyncDbContext.AddRangeDbContext.AddRangeAsyncили эквивалентных методов.DbSet<TEntity>

Совет

Эти методы работают одинаково в контексте отслеживания изменений. Дополнительные сведения см. в разделе "Дополнительные Отслеживание изменений функции".

Например, чтобы начать отслеживание нового блога:

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

При проверке представления отладки отслеживания изменений после этого вызова показано, что контекст отслеживает новую сущность в Added состоянии:

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

Однако методы Add не просто работают над отдельной сущностью. Они на самом деле начинают отслеживать весь граф связанных сущностей, помещая их все в Added состояние. Например, чтобы вставить новый блог и связанные новые записи:

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..."
            }
        }
    });

Теперь контекст отслеживает все эти сущности следующим образом 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}

Обратите внимание, что явные значения заданы для Id ключевых свойств в приведенных выше примерах. Это связано с тем, что модель здесь настроена для использования явно заданных значений ключей, а не автоматически созданных значений ключей. При использовании созданных ключей перед вызовом Addнеобходимо явно задать свойства ключа. Затем эти значения ключей вставляются при вызове SaveChanges. Например, при использовании 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);

Все эти сущности отслеживаются в Unchanged состоянии после завершения SaveChanges, так как эти сущности теперь существуют в базе данных:

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}

Созданные значения ключей

Как упоминание выше, целые числа и свойства ключа GUID настроены для использования автоматически созданных значений ключей по умолчанию. Это означает, что приложение не должно явно задавать значение ключа. Например, чтобы вставить новый блог и отправить все с созданными значениями ключей:

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..."
            }
        }
    });

Как и в случае с явными значениями ключей, контекст теперь отслеживает все эти сущности как 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}

Обратите внимание, что временные значения ключей были созданы для каждой сущности. Эти значения используются EF Core до вызова SaveChanges, когда реальные значения ключей считываются из базы данных. Например, при использовании 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();

После завершения SaveChanges все сущности были обновлены со своими реальными значениями ключей и отслеживаются в 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}

Это точно то же конечное состояние, что и предыдущий пример, который использовал явные значения ключей.

Совет

Явное значение ключа по-прежнему можно задать даже при использовании созданных значений ключей. Затем EF Core попытается вставить это значение ключа. Некоторые конфигурации базы данных, включая SQL Server с столбцами удостоверений, не поддерживают такие вставки и будут вызываться (см. эти документы для обходного решения).

Присоединение существующих сущностей

Явные значения ключей

Сущности, возвращаемые из запросов, отслеживаются в Unchanged состоянии. Состояние Unchanged означает, что сущность не была изменена с момента запроса. Отключенная сущность, возможно, возвращенная из веб-клиента в HTTP-запросе, может быть помещена в это состояние с помощью DbContext.AttachDbContext.AttachRangeлибо , либо эквивалентных методов.DbSet<TEntity> Например, чтобы начать отслеживание существующего блога:

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

Примечание.

Ниже приведены примеры создания сущностей явным образом с new простотой. Как правило, экземпляры сущностей будут поступать из другого источника, например десериализация из клиента или создание из данных в HTTP Post.

При проверке представления отладки отслеживания изменений после этого вызова показано, что сущность отслеживается в Unchanged состоянии:

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

Точно так же Add, Attach фактически задает весь граф подключенных сущностей к состоянию Unchanged . Например, для присоединения существующего блога и связанных с ними записей:

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..."
            }
        }
    });

Теперь контекст отслеживает все эти сущности следующим образом 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}

Вызов SaveChanges на этом этапе не будет иметь эффекта. Все сущности помечены как Unchanged, поэтому в базе данных ничего не нужно обновлять.

Созданные значения ключей

Как упоминание выше, целые числа и свойства ключа GUID настроены для использования автоматически созданных значений ключей по умолчанию. Это имеет основное преимущество при работе с отключенными сущностями: значение ключа без набора указывает, что сущность еще не вставлена в базу данных. Это позволяет отслеживанию изменений автоматически обнаруживать новые сущности и помещать их в Added состояние. Например, рассмотрите возможность присоединения этого графа блога и записей:

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..."
            },
        }
    });

В блоге имеется ключевое значение 1, указывающее, что он уже существует в базе данных. Два из записей также имеют ключевые значения, но третий не имеет значения. EF Core увидит это значение ключа как 0, значение CLR по умолчанию для целого числа. Это приводит к тому, что EF Core помечает новую сущность как Added не 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...'

Вызов SaveChanges на этом этапе ничего не делает с Unchanged сущностями, но вставляет новую сущность в базу данных. Например, при использовании 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();

Важно отметить, что при создании значений ключей EF Core может автоматически отличаться от существующих сущностей в отключенном графе. При использовании созданных ключей EF Core всегда будет вставлять сущность, если эта сущность не имеет значения ключа.

Обновление существующих сущностей

Явные значения ключей

DbContext.Update, DbContext.UpdateRangeи эквивалентные методы для поведения точно так же, как Attach описанные выше методыDbSet<TEntity>, за исключением того, что сущности помещаются в Modified состояние вместо Unchanged состояния. Например, чтобы начать отслеживание существующего блога следующим Modifiedобразом:

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

При проверке представления отладки отслеживания изменений после этого вызова показано, что контекст отслеживает эту сущность в Modified состоянии:

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

Точно так же, как и Add , фактически помечает весь граф связанных сущностей как Modified. UpdateAttach Например, для присоединения существующего блога и связанных с ними записей как 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..."
            }
        }
    });

Теперь контекст отслеживает все эти сущности следующим образом 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}

Вызов SaveChanges на этом этапе приведет к отправке обновлений в базу данных для всех этих сущностей. Например, при использовании 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();

Созданные значения ключей

Как и при использовании Attach, созданные значения ключей имеют то же основное преимущество: Updateзначение ключа без набора указывает на то, что сущность является новой и еще не вставлена в базу данных. Как и в случае Attach, это позволяет DbContext автоматически обнаруживать новые сущности и помещать их в Added состояние. Например, рассмотрите возможность вызова Update с помощью этого графа блога и записей:

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..."
            },
        }
    });

Как и в Attach примере, запись без значения ключа обнаруживается как новая и задана в Added состоянии. Другие сущности помечены как 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}

Вызов SaveChanges на этом этапе приведет к отправке обновлений в базу данных для всех существующих сущностей, а новая сущность вставляется. Например, при использовании 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();

Это очень простой способ создания обновлений и вставок из отключенного графа. Однако он приводит к обновлению или вставке в базу данных для каждого свойства каждой отслеживаемой сущности, даже если некоторые значения свойств не были изменены. Не бойтесь этого; для многих приложений с небольшими графами это может быть простой и прагматичный способ создания обновлений. Это говорится, что другие более сложные шаблоны иногда могут привести к более эффективным обновлениям, как описано в разделе "Разрешение удостоверений" в EF Core.

Удаление существующих сущностей

Чтобы сущность была удалена с помощью SaveChanges, ее необходимо отслеживать в Deleted состоянии. Сущности обычно помещаются в Deleted состояние путем вызова одного из DbContext.RemoveDbContext.RemoveRangeметодов или эквивалентных методовDbSet<TEntity>. Например, чтобы пометить существующую запись как Deleted:

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

При проверке представления отладки отслеживания изменений после этого вызова показано, что контекст отслеживает сущность в Deleted состоянии:

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

Эта сущность будет удалена при вызове SaveChanges. Например, при использовании SQLite:

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

После завершения SaveChanges удаленная сущность отсоединяется от DbContext, так как она больше не существует в базе данных. Таким образом, представление отладки пусто, так как сущности не отслеживаются.

Удаление зависимых или дочерних сущностей

Удаление зависимых или дочерних сущностей из графа проще, чем удаление сущностей субъекта или родительского объекта. Дополнительные сведения см. в следующем разделе и изменении внешних ключей и навигаций .

Нестандартно вызывать Remove сущность, созданную с newпомощью . Кроме того, в отличие от Addсущности AttachRemoveUpdate, которая еще не отслеживается в Unchanged состоянии или Modified не отслеживается. Вместо этого обычно можно отслеживать одну сущность или граф связанных сущностей, а затем вызывать Remove сущности, которые необходимо удалить. Этот граф отслеживаемых сущностей обычно создается следующим образом:

  1. Выполнение запроса для сущностей
  2. Attach Использование или Update методы на графе отключенных сущностей, как описано в предыдущих разделах.

Например, код в предыдущем разделе, скорее всего, получит сообщение от клиента, а затем сделайте следующее:

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

Это ведет себя точно так же, как и в предыдущем примере, так как вызов Remove не отслеживаемой сущности приводит к тому, что она сначала будет присоединена, а затем помечена как Deleted.

В более реалистичных примерах граф сущностей сначала присоединен, а затем некоторые из этих сущностей помечены как удаленные. Например:

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

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

Все сущности помечаются как Unchanged, кроме вызываемого 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}

Эта сущность будет удалена при вызове SaveChanges. Например, при использовании SQLite:

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

После завершения SaveChanges удаленная сущность отсоединяется от DbContext, так как она больше не существует в базе данных. Другие сущности 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}

Удаление субъектов или родительских сущностей

Каждая связь, которая соединяет два типа сущностей, имеет основной или родительский конец, а также зависимый или дочерний конец. Зависимые или дочерние сущности — это сущность с свойством внешнего ключа. В связи "один ко многим" субъект или родитель находится на стороне "один", и зависимый/дочерний элемент находится на стороне "многие". Дополнительные сведения см. в разделе "Связи ".

В предыдущих примерах мы удаляли запись, которая является зависимой или дочерней сущностью в связи "один ко многим". Это относительно просто, так как удаление зависимой или дочерней сущности не оказывает никакого влияния на другие сущности. С другой стороны, удаление основной или родительской сущности также должно повлиять на любые зависимые или дочерние сущности. Это не позволит оставить значение внешнего ключа, ссылающееся на значение первичного ключа, которое больше не существует. Это недопустимое состояние модели и приводит к ошибке ограничения ссылки в большинстве баз данных.

Это недопустимое состояние модели можно обрабатывать двумя способами:

  1. Задание значений FK значение NULL. Это означает, что зависимые или дочерние объекты больше не связаны с любым субъектом или родителем. Это значение по умолчанию для необязательных связей, в которых внешний ключ должен иметь значение NULL. Если внешний ключ обычно не имеет значения NULL, недопустимо для обязательных связей, где внешний ключ обычно не допускает значение NULL.
  2. Удаление зависимых или дочерних элементов. Это значение по умолчанию для обязательных связей, а также допустимо для необязательных связей.

Дополнительные сведения об отслеживании изменений и связях см. в статье об изменении внешних ключей и навигации.

Необязательные связи

Свойство Post.BlogId внешнего ключа допускает значение NULL в модели, используемой нами. Это означает, что связь необязательна, поэтому поведение по умолчанию EF Core заключается в том, чтобы задать BlogId свойства внешнего ключа значение NULL при удалении блога. Например:

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

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

Проверка представления отладки отслеживания изменений после вызова Remove показывает, что, как ожидалось, блог теперь помечен как 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>

Более интересно, что все связанные записи теперь помечены как Modified. Это связано с тем, что свойство внешнего ключа в каждой сущности имеет значение NULL. Вызов SaveChanges обновляет значение внешнего ключа для каждой записи до null в базе данных, прежде чем удалять блог:

-- 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();

После завершения SaveChanges удаленная сущность отсоединяется от DbContext, так как она больше не существует в базе данных. Другие сущности теперь помечены как Unchanged значения null внешнего ключа, которые соответствуют состоянию базы данных:

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>

Обязательные связи

Post.BlogId Если свойство внешнего ключа не допускает значение NULL, связь между блогами и записями становится "обязательной". В этой ситуации EF Core по умолчанию удаляет зависимые или дочерние сущности при удалении субъекта или родителя. Например, удаление блога со связанными записями, как показано в предыдущем примере:

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

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

Проверка представления отладки отслеживания изменений после вызова Remove показывает, что, как ожидалось, блог снова помечается как 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}

Более интересно в этом случае заключается в том, что все связанные должности также были помечены как Deleted. Вызов SaveChanges приводит к удалению блога и всех связанных записей из базы данных:

-- 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;

После завершения SaveChanges все удаленные сущности отсоединяются от DbContext, так как они больше не существуют в базе данных. Поэтому выходные данные из представления отладки пусты.

Примечание.

Этот документ только царапает поверхность работы с связями в EF Core. Дополнительные сведения о связях моделирования и изменении внешних ключей и навигаций см. в разделе "Связи" для получения дополнительных сведений об обновлении и удалении зависимых и дочерних сущностей при вызове SaveChanges.

Настраиваемое отслеживание с помощью TrackGraph

ChangeTracker.TrackGraph работает так Add, Attach и Update за исключением того, что он создает обратный вызов для каждого экземпляра сущности перед отслеживанием. Это позволяет использовать пользовательскую логику при определении способа отслеживания отдельных сущностей в графе.

Например, рассмотрим правило EF Core, которое используется при отслеживании сущностей с созданными значениями ключей: если значение ключа равно нулю, то сущность является новой и должна быть вставлена. Давайте расширим это правило, чтобы сказать, является ли ключевое значение отрицательным, то сущность должна быть удалена. Это позволяет изменять значения первичного ключа в сущностях отключенного графа, чтобы пометить удаленные сущности:

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;

Затем этот отключенный граф можно отслеживать с помощью 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();
}

Для каждой сущности в графе код выше проверка значение первичного ключа перед отслеживанием сущности. Для неустановленных значений ключей (ноль) код делает то, что EF Core обычно делает. То есть, если ключ не задан, сущность помечается как Added. Если задан ключ и значение не является отрицательным, сущность помечается как Modified. Однако если найдено отрицательное значение ключа, то его реальное, не отрицательное значение восстанавливается, а сущность отслеживается как Deleted.

Выходные данные выполнения этого кода:

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

Примечание.

Для простоты этот код предполагает, что каждая сущность имеет целочисленное свойство первичного ключа Id. Это может быть кодифицировано в абстрактный базовый класс или интерфейс. Кроме того, свойство или свойства первичного ключа можно получить из IEntityType метаданных, чтобы этот код работал с любым типом сущности.

TrackGraph имеет две перегрузки. В приведенной выше простой перегрузке EF Core определяет, когда следует остановить обход графа. В частности, он перестает посещать новые связанные сущности из заданной сущности, когда эта сущность уже отслеживается или когда обратный вызов не запускает отслеживание сущности.

Расширенная перегрузка ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>), имеет обратный вызов, который возвращает логическое значение. Если обратный вызов возвращает значение false, то обход графа останавливается, в противном случае он продолжается. Будьте внимательны, чтобы избежать бесконечных циклов при использовании этой перегрузки.

Расширенная перегрузка также позволяет предоставлять состояние в TrackGraph, а затем это состояние передается каждому обратному вызову.