Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Каждая инстанция DbContext отслеживает изменения, внесенные в сущности. Эти отслеживаемые сущности, в свою очередь, управляют изменениями базы данных при SaveChanges вызове.
Отслеживание изменений Entity Framework Core (EF Core) лучше всего работает, если один и тот же DbContext экземпляр используется для запроса сущностей и обновления их путем вызова SaveChanges. Это связано с тем, что EF Core автоматически отслеживает состояние запрашиваемых сущностей, а затем обнаруживает любые изменения, внесенные в эти сущности при вызове SaveChanges. Этот подход рассматривается в разделе "Отслеживание изменений" в EF Core.
Tip
В этом документе предполагается знание состояний сущностей и основ отслеживания изменений в EF Core. Дополнительные сведения об этих разделах см. в статье об отслеживании изменений в EF Core .
Tip
Вы можете запускать и отлаживать весь код в этом документе, скачав пример кода из GitHub.
Introduction
Сущности могут быть явно присоединены к DbContext таким образом, что контекст затем отслеживает эти сущности. Это в первую очередь полезно, когда:
- Создание новых сущностей, которые будут вставлены в базу данных.
- Повторное присоединение отключенных сущностей, которые ранее запрашивались другим экземпляром DbContext.
Первое из них потребуется большинству приложений и в первую очередь обрабатывается методами DbContext.Add .
Второй требуется только приложениям, которые изменяют сущности или их связи , пока сущности не отслеживаются. Например, веб-приложение может отправлять сущности в веб-клиент, где пользователь вносит изменения и отправляет сущности обратно. Эти сущности называются "отключенными", так как изначально они были запрошены из DbContext, но затем были отключены от этого контекста при отправке клиенту.
Теперь веб-приложение должно повторно присоединить эти сущности, чтобы они снова отслеживались и указывали на изменения таким образом, чтобы SaveChanges мог внести необходимые обновления в базу данных. Это в первую очередь обрабатывается методами DbContext.Attach и DbContext.Update.
Tip
Присоединение сущностей к тому же экземпляру DbContext, из которого они были запрошены, обычно не требуется. Не выполняйте регулярно запросы без отслеживания и затем присоединяйте возвращаемые сущности к тому же контексту. Это будет медленнее, чем при использовании запроса отслеживания, а также может привести к проблемам, таким как отсутствие значений теневых свойств, что затрудняет их правильное исправление.
Сгенерированные значения ключей в отличие от явных
По умолчанию свойства целочисленных и ключей GUID настраиваются для использования автоматически созданных значений ключей. Это имеет важное преимущество для отслеживания изменений: если значение ключа не установлено, это указывает на то, что сущность является "новой". Под "новым" мы подразумеваем, что он еще не вставлен в базу данных.
Две модели используются в следующих разделах. Настройка первой полягает в не использовать созданные значения ключей.
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.AddRange, DbContext.AddAsync, DbContext.AddRangeAsync или эквивалентных методов на DbSet<TEntity>.
Tip
Эти методы работают одинаково в контексте отслеживания изменений. Дополнительные сведения см. в разделе "Дополнительные возможности отслеживания изменений ".
Например, чтобы начать отслеживание нового блога:
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}
Это точно то же конечное состояние, что и предыдущий пример, который использовал явные значения ключей.
Tip
Явное значение ключа по-прежнему можно задать даже при использовании созданных значений ключей. Затем EF Core попытается использовать этот ключ, чтобы вставить значение. Некоторые конфигурации базы данных, включая SQL Server со столбцами идентификаторов, не поддерживают такие вставки и вызовут исключение (см. эти документы для обходного решения).
Присоединение существующих сущностей
Явные значения ключей
Сущности, возвращаемые из запросов, отслеживаются в рамках состояния Unchanged. Состояние Unchanged означает, что сущность не была изменена с момента запроса. Отключенная сущность, возможно, возвращенная из веб-клиента в HTTP-запросе, может быть помещена в это состояние с помощью DbContext.Attach, DbContext.AttachRange либо эквивалентных методов на DbSet<TEntity>. Например, чтобы начать отслеживание существующего блога:
context.Attach(
new Blog { Id = 1, Name = ".NET Blog", });
Note
Примеры здесь явно создают сущности с 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, и эквивалентные методы на DbSet<TEntity> ведут себя точно так же, как методы Attach, описанные выше, за исключением того, что сущности помещаются в состояние 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: []
Точно так же, как и AddAttach, Update фактически помечает весь граф связанных сущностей как Modified. Например, для присоединения существующего блога и связанных с ним записей как 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.Remove, DbContext.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, Attach и Update, бывает редко, чтобы Remove применялось к сущности, которая еще не отслеживается в состоянии Unchanged или Modified. Вместо этого обычно можно отслеживать одну сущность или граф связанных сущностей, а затем вызывать Remove сущности, которые необходимо удалить. Этот граф отслеживаемых сущностей обычно создается следующим образом:
- Выполнение запроса для сущностей
- Использование методов
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}
Удаление основных или родительских сущностей
Каждая связь, которая соединяет два типа сущностей, имеет основной или родительский конец, а также зависимый или дочерний конец. Зависимая или дочерняя сущность — это сущность с атрибутом внешнего ключа. В отношении "один ко многим" главный элемент или родитель находится на стороне "один", а зависимый или дочерний элемент находится на стороне "многие". Дополнительные сведения см. в разделе "Связи ".
В предыдущих примерах мы удаляли запись, которая является зависимой или дочерней сущностью в связи "один ко многим". Это относительно просто, так как удаление зависимой или дочерней сущности не оказывает никакого влияния на другие сущности. С другой стороны, удаление основной или родительской сущности также должно повлиять на любые зависимые или дочерние сущности. Если это не сделать, значение внешнего ключа будет ссылаться на значение первичного ключа, которое больше не существует. Это недопустимое состояние модели и приводит к ошибке ограничения ссылки в большинстве баз данных.
Это недопустимое состояние модели можно обрабатывать двумя способами:
- Установить значение внешнего ключа в NULL. Это означает, что зависимые лица или дети больше не связаны с каким-либо родителем. Это значение по умолчанию для необязательных связей, в которых внешний ключ должен иметь значение NULL. Установка внешнего ключа в значение NULL недопустима для обязательных связей, где внешний ключ обычно не допускает значение NULL.
- Удаление зависимых или дочерних элементов. Это значение по умолчанию для обязательных связей, а также допустимо для необязательных связей.
Дополнительные сведения об отслеживании изменений и связях см. в статье об изменении внешних ключей и навигации .
Необязательные связи
Свойство 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, так как они больше не существуют в базе данных. Поэтому выходные данные из представления отладки пусты.
Note
Этот документ только поверхностно рассматривает взаимодействие с связями в 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 async Task 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}");
});
await context.SaveChangesAsync();
}
Для каждой сущности в графе приведенный выше код проверяет значение первичного ключа перед отслеживанием сущности. Для неустановленных значений ключей (ноль) код делает то, что 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
Note
Для простоты этот код предполагает, что каждая сущность имеет целочисленное свойство первичного ключа Id. Это может быть кодифицировано в абстрактный базовый класс или интерфейс. Кроме того, свойство или свойства первичного ключа можно получить из IEntityType метаданных, чтобы этот код работал с любым типом сущности.
TrackGraph имеет две перегрузки. В приведенной выше простой перегрузке EF Core определяет, когда следует остановить обход графа. В частности, он перестает посещать новые связанные сущности из заданной сущности, когда эта сущность уже отслеживается или когда обратный вызов не запускает отслеживание сущности.
Расширенная перегрузка ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>), имеет обратный вызов, который возвращает логическое значение. Если обратный вызов возвращает значение false, то обход графа останавливается, в противном случае он продолжается. Будьте внимательны, чтобы избежать бесконечных циклов при использовании этой перегрузки.
Продвинутая перегрузка также позволяет передавать состояние в TrackGraph, после чего это состояние направляется каждому колбэку.