断开连接的实体

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 的一部分或请求中包含的其他一些状态。