DbContext 实例将自动跟踪从数据库返回的实体。 调用 SaveChanges 时,将检测对这些实体所做的更改,并根据需要更新数据库。 有关详细信息,请参阅基本保存和相关数据。
但是,有时使用一个上下文实例查询实体,然后使用其他实例进行保存。 这种情况通常发生在“断开连接”方案中,例如在 Web 应用程序中查询实体,然后发送到客户端,进行修改后作为请求发送回服务器,最后保存。 在这种情况下,第二个上下文实例需要知道实体是新的(应插入的)还是现有的(应更新)。
小窍门
可以在 GitHub 上查看本文 的示例 。
小窍门
EF Core 只能跟踪具有给定主键值的任何实体的一个实例。 避免此问题的最佳方法是为每个工作单元使用短暂的上下文,使上下文以空状态启动,附加实体并保存这些实体,然后销毁和丢弃上下文。
标识新实体
客户端标识新实体
最简单的处理情况是客户端通知服务器实体是新的还是现有的。 例如,插入新实体的请求通常不同于更新现有实体的请求。
本部分的其余部分介绍需要以某种其他方式确定插入或更新的情况。
使用自动生成的密钥
自动生成的键的值通常用于确定是否需要插入或更新实体。 如果键尚未设置(即,它仍具有 NULL、零等的 CLR 默认值),则实体必须是新的且需要插入。 另一方面,如果设置了键值,则它必须已保存,现在需要更新。 换句话说,如果密钥有一个值,那么实体已被查询并发送到客户端,现在已经返回以进行更新。
当实体类型已知时,很容易检查未设置的键:
public static bool IsItNew(Blog blog)
=> blog.BlogId == 0;
EF 还内置了一种方法来执行针对任何实体类型和键类型的操作。
public static bool IsItNew(DbContext context, object entity)
=> !context.Entry(entity).IsKeySet;
小窍门
即使实体处于“已添加”状态,也会在上下文跟踪实体后立即设置键。 这在遍历实体图并决定如何处理每个实体时(例如在使用 TrackGraph API 时)非常有帮助。 只有在进行任何调用以跟踪实体 之前 ,才应以此处所示的方式使用键值。
使用其他密钥
如果未自动生成键值,则需要一些其他机制来标识新实体。 有两种常规方法可以实现此目的:
- 查询实体
- 从客户端传递标志
若要查询实体,只需使用 Find 方法:
public static async Task<bool> IsItNew(BloggingContext context, Blog blog)
=> (await context.Blogs.FindAsync(blog.BlogId)) == null;
显示从客户端传递标志的完整代码不在本文档的范围之内。 在 Web 应用中,它通常意味着对不同的作发出不同的请求,或在请求中传递某些状态,然后在控制器中提取它。
保存单个实体
如果已知是否需要插入或更新,则可以适当地使用“添加”或“更新”:
public static async Task Insert(DbContext context, object entity)
{
context.Add(entity);
await context.SaveChangesAsync();
}
public static async Task Update(DbContext context, object entity)
{
context.Update(entity);
await context.SaveChangesAsync();
}
但是,如果实体使用自动生成的键值,则可以将 Update 方法用于这两种情况:
public static async Task InsertOrUpdate(DbContext context, object entity)
{
context.Update(entity);
await context.SaveChangesAsync();
}
Update 方法通常标记要更新的实体,而不是插入。 但是,如果实体具有自动生成的键,并且未设置任何键值,则实体会自动标记为插入。
如果实体未使用自动生成的密钥,则应用程序必须决定是否应插入或更新实体:例如:
public static async Task InsertOrUpdate(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs.FindAsync(blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
}
await context.SaveChangesAsync();
}
以下步骤如下:
- 如果 Find 返回 null,则数据库尚未包含具有此 ID 的博客,因此我们调用 Add 标记它进行插入。
- 如果 Find 返回实体,则它存在于数据库中,并且上下文现在正在跟踪现有实体
- 然后,我们使用 SetValues 将此实体上所有属性的值设置为来自客户端的属性。
- SetValues 调用将标记实体,以便在需要时进行更新。
小窍门
SetValues 仅会将与跟踪实体中值不同的属性标记为已修改。 这意味着发送更新时,只会更新实际更改的列。 (如果没有更改,则不会发送任何更新。
使用图形
标识解析
如上所述,EF Core 只能跟踪具有给定主键值的任何实体的一个实例。 使用图时,图应该最好创建得以便保持其不变性,并且上下文应仅用于一个工作单元。 如果图形包含重复项,则在将图形发送到 EF 以将多个实例合并到一个实例之前,必须对其进行处理。 在实例具有冲突的值和关系的情况下,这可能并不简单,因此应尽快在应用程序管道中合并重复项以避免冲突解决。
所有新实体/所有现有实体
使用图形的示例是插入或更新博客及其相关文章的集合。 如果图中的所有实体都应被插入,或全部应被更新,那么该过程与上述单个实体的过程相同。 例如,关于博客和文章的图表可以这样创建:
var blog = new Blog
{
Url = "http://sample.com", Posts = new List<Post> { new Post { Title = "Post 1" }, new Post { Title = "Post 2" }, }
};
可以插入如下:
public static async Task InsertGraph(DbContext context, object rootEntity)
{
context.Add(rootEntity);
await context.SaveChangesAsync();
}
调用添加操作将标记博客及其中所有要插入的文章。
同样,如果需要更新图形中的所有实体,则可以使用更新:
public static async Task UpdateGraph(DbContext context, object rootEntity)
{
context.Update(rootEntity);
await context.SaveChangesAsync();
}
博客及其所有文章都将标记为要更新。
混合使用新实体和现有实体
使用自动生成的密钥时,即使图形同时包含需要插入和需要更新的实体,Update仍可以再次用于插入和更新。
public static async Task InsertOrUpdateGraph(DbContext context, object rootEntity)
{
context.Update(rootEntity);
await context.SaveChangesAsync();
}
如果图形、博客或文章中没有设置键值,更新将标记任何实体以供插入,而所有其他实体将标记为更新。
像之前一样,当不使用自动生成的密钥时,可以使用查询和一些处理:
public static async Task InsertOrUpdateGraph(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs
.Include(b => b.Posts)
.FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
foreach (var post in blog.Posts)
{
var existingPost = existingBlog.Posts
.FirstOrDefault(p => p.PostId == post.PostId);
if (existingPost == null)
{
existingBlog.Posts.Add(post);
}
else
{
context.Entry(existingPost).CurrentValues.SetValues(post);
}
}
}
await context.SaveChangesAsync();
}
处理删除
处理删除操作可能很棘手,因为通常实体的缺失意味着应该进行删除。 解决此问题的一种方法是使用“软删除”,以便实体被标记为已删除,而不是实际删除。 然后,删除与更新相同。 可以使用 查询筛选器实现软删除。
对于真正的删除,常见模式是使用查询模式的扩展来执行本质上是图形差异的内容。 例如:
public static async Task InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs
.Include(b => b.Posts)
.FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
foreach (var post in blog.Posts)
{
var existingPost = existingBlog.Posts
.FirstOrDefault(p => p.PostId == post.PostId);
if (existingPost == null)
{
existingBlog.Posts.Add(post);
}
else
{
context.Entry(existingPost).CurrentValues.SetValues(post);
}
}
foreach (var post in existingBlog.Posts)
{
if (!blog.Posts.Any(p => p.PostId == post.PostId))
{
context.Remove(post);
}
}
}
await context.SaveChangesAsync();
}
TrackGraph
在内部,添加、附加和更新使用图形遍历,确定每个实体是否应将其标记为“已添加”(要插入)、已修改(要更新)、未更改(不执行任何作)或“删除”(要删除)。 此机制通过 TrackGraph API 公开。 例如,假设当客户端发送回实体图时,它会在每个实体上设置一些标志,指示如何处理它。 然后,TrackGraph 可用于处理此标志:
public static async Task SaveAnnotatedGraph(DbContext context, object rootEntity)
{
context.ChangeTracker.TrackGraph(
rootEntity,
n =>
{
var entity = (EntityBase)n.Entry.Entity;
n.Entry.State = entity.IsNew
? EntityState.Added
: entity.IsChanged
? EntityState.Modified
: entity.IsDeleted
? EntityState.Deleted
: EntityState.Unchanged;
});
await context.SaveChangesAsync();
}
为了简单起见,旗帜仅显示为示例的一部分。 通常,标识通常是 DTO 的一部分或请求中包含的其他一些状态。