跟踪与 No-Tracking 查询

跟踪行为控制 Entity Framework Core 是否在其更改跟踪器中保留有关实体实例的信息。 如果跟踪了实体,则实体中检测到的任何更改将在SaveChanges期间持久化到数据库中。 EF Core 还修复了跟踪查询结果中的实体与更改跟踪器中的实体之间的导航属性。

注释

无主键实体类型永远不会被跟踪。 无论本文在何处提到实体类型,它指的是那些定义了键的实体类型。

小窍门

可以在 GitHub 上查看本文 的示例

跟踪查询

默认情况下,返回实体类型的查询会跟踪。 跟踪查询意味着对实体实例所做的任何更改都由SaveChanges保留。 在以下示例中,在SaveChanges过程中检测到博客评分的更改并将其保存到数据库:

var blog = await context.Blogs.SingleOrDefaultAsync(b => b.BlogId == 1);
blog.Rating = 5;
await context.SaveChangesAsync();

当结果在跟踪查询中返回时,EF Core 会检查实体是否已在上下文中。 如果 EF Core 找到现有实体,则返回相同的实例,这可能会减少内存,并且比无跟踪查询更快。 EF Core 不会用数据库值覆盖条目中实体属性的当前值和原始值。 如果在上下文中找不到该实体,EF Core 将创建一个新的实体实例并将其附加到上下文。 查询结果不包含添加到上下文但尚未保存到数据库的任何实体。

无跟踪查询

在只读方案中使用结果时,无跟踪查询非常有用。 它们通常执行速度更快,因为无需设置更改跟踪信息。 如果从数据库检索到的实体不需要更新,则应使用无跟踪查询。 可以将单个查询设置为无跟踪。 不跟踪查询还会根据数据库中的内容给出结果,而不考虑任何本地修改或添加的实体。

var blogs = await context.Blogs
    .AsNoTracking()
    .ToListAsync();

可以在上下文实例级别更改默认跟踪行为:

context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

var blogs = await context.Blogs.ToListAsync();

下一部分介绍无跟踪查询何时可能比跟踪查询效率低。

标识解析

由于跟踪查询使用更改跟踪器,EF Core 在跟踪查询中执行标识解析。 具体化实体时,EF Core 会从更改跟踪器中返回相同的实体实例(如果它已被跟踪)。 如果结果包含同一实体多次,则会为每次出现返回同一实例。 无跟踪查询:

  • 请勿使用更改跟踪器,也不执行标识解析。
  • 当同一实体多次出现在结果中时,仍然返回新的实体实例。

可以在同一查询中组合跟踪和无跟踪。 也就是说,可以有一个无跟踪查询,该查询在结果中执行身份解析。 与可查询运算符一样 AsNoTracking ,我们添加了另一个运算符 AsNoTrackingWithIdentityResolution<TEntity>(IQueryable<TEntity>)。 枚举中也添加了一个关联条目QueryTrackingBehavior。 当查询配置为使用身份解析且无跟踪时,在生成查询结果时,会在后台使用独立的更改跟踪器,以确保每个实例仅被具体化一次。 由于此更改跟踪器与上下文中的更改跟踪器不同,因此上下文不会跟踪结果。 完全枚举查询后,更改跟踪器会超出范围并根据需要进行垃圾回收。

var blogs = await context.Blogs
    .AsNoTrackingWithIdentityResolution()
    .ToListAsync();

配置默认跟踪行为

如果发现自己更改了许多查询的跟踪行为,您可能希望改为修改默认值:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying.Tracking;Trusted_Connection=True;ConnectRetryCount=0")
        .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}

这会使所有查询默认不被追踪。 仍可以添加 AsTracking 以进行特定查询跟踪。

跟踪和自定义投影

即使查询的结果类型不是实体类型,EF Core 也会默认跟踪结果中包含的实体类型。 在返回匿名类型的以下查询中,将跟踪结果集中的 Blog 实例。

var blog = context.Blogs
    .Select(
        b =>
            new { Blog = b, PostCount = b.Posts.Count() });

如果结果集包含来自 LINQ 组合的实体类型,EF Core 将跟踪它们。

var blog = context.Blogs
    .Select(
        b =>
            new { Blog = b, Post = b.Posts.OrderBy(p => p.Rating).LastOrDefault() });

如果结果集不包含任何实体类型,则不会执行任何跟踪。 在以下查询中,我们将返回一个匿名类型,其中包含实体的某些值(但没有实际实体类型的实例)。 在查询中没有被跟踪的实体。

var blog = context.Blogs
    .Select(
        b =>
            new { Id = b.BlogId, b.Url });

EF Core 支持在最高级别的投影中执行客户端评估。 如果 EF Core 实例化一个实体实例用于客户端评估,则该实例会被跟踪。 在这里,由于我们将实体传递到 blog 客户端方法 StandardizeURL,EF Core 也会跟踪博客实例。

var blogs = await context.Blogs
    .OrderByDescending(blog => blog.Rating)
    .Select(
        blog => new { Id = blog.BlogId, Url = StandardizeUrl(blog) })
    .ToListAsync();
public static string StandardizeUrl(Blog blog)
{
    var url = blog.Url.ToLower();

    if (!url.StartsWith("http://"))
    {
        url = string.Concat("http://", url);
    }

    return url;
}

EF Core 不会跟踪结果中包含的无键实体实例。 但 EF Core 会根据上述规则使用键跟踪实体类型的其他所有实例。

以前的版本

在版本 3.0 之前,EF Core 在跟踪的完成方式方面存在一些差异。 显著差异如下:

  • “客户端与服务器评估 ”页中所述,EF Core 在版本 3.0 之前的查询的任何部分中都支持客户端评估。 客户端评估导致实体具体化,而实体不是结果的一部分。 因此,EF Core 分析了结果以检测要跟踪的内容。此设计有一定的差异,如下所示:

    • 在投影中,客户端评估导致了具体化,但由于未返回具体化的实体实例,该实例未被跟踪。 以下示例未跟踪 blog 实体。

      var blogs = await context.Blogs
          .OrderByDescending(blog => blog.Rating)
          .Select(
              blog => new { Id = blog.BlogId, Url = StandardizeUrl(blog) })
          .ToListAsync();
      
    • 在某些情况下,EF Core 未跟踪 LINQ 组合中出现的对象。 以下示例未跟踪 Post

      var blog = context.Blogs
          .Select(
              b =>
                  new { Blog = b, Post = b.Posts.OrderBy(p => p.Rating).LastOrDefault() });
      
  • 每当查询结果包含无键实体类型时,整个查询都变为不跟踪。 这意味着结果中具有键的实体类型也没有被跟踪。

  • EF Core 用于在无跟踪查询中执行标识解析。 它使用弱引用跟踪已返回的实体。 因此,如果一个结果集多次包含同一实体,那么您每次都会获得相同的实例。 不过,如果以前具有相同标识的结果生命周期结束并被垃圾回收,EF Core 就返回一个新实例。