EF 的 AsNoTracking() 不会阻止生成的实体被跟踪(+ 仍然对它们进行延迟加载)

EF's AsNoTracking() doesn't prevent the resulting entities to be tracked (+ still have lazy loading on them)

我在aync方法中有以下代码:

using System.Data.Entity;

// ...

protected internal async Task<Customer> GetCustomerAsync(int entityId, CancellationToken cancellationToken)
{
    IQueryable<Customer> query = (await this.Repository.GetAll(cancellationToken)).Where(e => e.Id == entityId);

    query = query.Include(e => e.Partners);

    string sql = query.ToString();

    return await query.AsNotTracking().FirstOrDefaultAsync(cancellationToken);
}

sql 变量包含如下内容:

SELECT 
[Project1].[UserId] AS [UserId], 
[Project1].[EntityId] AS [EntityId], 
[Project1].[Id] AS [Id], 
[Project1].[OtherColumn] AS [OtherColumn], 
[Project1].[Name] AS [Name],
[Project1].[C1] AS [C1]
FROM ( SELECT 
    [Extent1].[UserId] AS [UserId], 
    [Extent1].[EntityId] AS [EntityId], 
    [Extent2].[Id] AS [Id], 
    [Extent2].[OtherColumn] AS [OtherColumn]
    [Extent3].[Id] AS [Id1], 
    [Extent3].[CustomerId] AS [CustomerId], 
    [Extent3].[PartnerId] AS [PartnerId], 
    CASE WHEN ([Extent3].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
    FROM   [op].[CustomerAcls] AS [Extent1]
    INNER JOIN [op].[Customers] AS [Extent2] ON [Extent1].[EntityId] = [Extent2].[Id]
    LEFT OUTER JOIN [op].[CustomerPartners] AS [Extent3] ON [Extent2].[Id] = [Extent3].[CustomerId]
    WHERE ([Extent1].[UserId] = @p__linq__0) AND ([Extent1].[AccessRight] >= @p__linq__1) AND ([Extent2].[Id] = @p__linq__2)
)  AS [Project1]
ORDER BY [Project1].[UserId] ASC, [Project1].[EntityId] ASC, [Project1].[Id] ASC, [Project1].[C1] ASC

更新 -----

仓库代码如下:

public class CustomerRepository<TAcl> : ICustomerRepository
    where TAcl : class, IAcl<Customer>, new()
{
    private readonly DbSet<TAcl> aclSet;

    public CustomerRepository(
        BranchDbContext branchDbContext,
        Func<BranchDbContext, DbSet<TAcl>> aclSetFunc)
    {   
        this.aclSet = aclSetFunc(branchDbContext);
    }

    public async Task<IQueryable<Customer>> GetAll(CancellationToken cancellationToken)
    {
        int minAccessRight = //...
        int currentUserId = //...

        return from acl in (from acl in this.aclSet
                       where acl.UserId == currentUserId &&
                             acl.AccessRight >= minAccessRight
                       select acl)
        select acl.Entity;
    }
}

------------

尽管使用了 AsNoTracking() 方法(参见 return 语句),尽管我确保存储库返回 LinqToEntities 查询(它尚未实现),生成的 returned 实例(方法 GetCustomerAsync 的)仍然具有延迟加载功能。

文档太含糊了,它说:

If the underlying query object does not have a AsNoTracking method, then calling this method will have no affect.

是否有人看到了我的代码中的问题所在或理解了文档中的内容?

谢谢

你是如何检查实体未被跟踪的。 AsNoTracking 方法的目的是从 entity framework 更改跟踪器中排除获取的记录(例如,当您仅将数据用于读取目的并希望防止意外地将数据持久保存在数据库中时)。

请参阅 AsNoTracking 文档 https://docs.microsoft.com/en-us/ef/core/querying/tracking

我建议检查 DbContext(在您 运行 查询之后)并检查您从数据库中获取的实体是否在更改跟踪器中被跟踪。

如果 entityIdCustomer table 的主键,请考虑使用 FindAsync 而不是 FirstOrDefaultAsync

这就是我编写方法的方式:

protected internal async Task<Customer> GetCustomerAsync(int entityId, CancellationToken cancellationToken)
{
    return this.Repository
        .AsNoTracking()
        .Include(e => e.Partners)
        .FindAsync(entityId, cancellationToken);
}

这并不一定表示实体已作为该查询的结果进行跟踪。如果启用延迟加载且导航属性为 virtual.

,EF 6 仍将在 non-tracked 结果中延迟加载引用的实体

作为父子集合的简单示例:

EF 6:

using (var context = new TestDbContext())
{
    var parent = context.Parents.AsNoTracking().Single(x => x.ParentId == 1);
    var count = parent.Children.Count;
}

此处启用了延迟加载并将子项声明为 virtual ICollection<Child>,EF 6 将在访问 parent.Children 时触发延迟加载调用。这可以通过 运行 使用断点针对您的数据库的分析器来验证,您将看到延迟加载调用结束。

为避免这种情况,您需要关闭延迟加载,或确保未将子 属性 声明为 virtual。在任何一种情况下,“计数”都会返回 0(或者如果未初始化为空列表则触发空引用)

对于 EF 核心,情况有些不同。默认情况下,EF Core 禁用延迟加载,因此“计数”将始终返回 0。如果您在 DbContext 中启用延迟加载代理,则上述代码将导致 InvalidOperationException,因为现在 AsNoTracking() 实体是正确的如果您尝试从分离的实例延迟加载,则检测为已分离并抛出此异常。 (如果 DbContext 仍在范围内,EF6 允许这样做)

EF6 和 EF Core 都不会 pre-populate 相关实体,这些实体在获取 NoTracking() 实例时可能已经被跟踪。

例如,假设延迟加载被禁用或non-virtual collection:

using (var context = new TestDbContext())
{
    var parent = context.Parents.Include(x => x.Children).AsNoTracking().Single(x => x.ParentId == 1);
    var count = parent.Children.Count;  // eager load, will return count.
}

using (var context = new TestDbContext())
{
    var parent = context.Parents.AsNoTracking().Single(x => x.ParentId == 1);
    var count = parent.Children.Count;  // 0.
}

using (var context = new TestDbContext())
{
    var children = context.Children.Where(x => x.ParentId == 1).ToList();
    var parent = context.Parents.Single(x => x.ParentId == 1);
    var count = parent.Children.Count;  // not eager or lazy loaded, will still return count. 
     // (This can be inaccurate if only *some* children were previously loaded.)
}

using (var context = new TestDbContext())
{
    var children = context.Children.Where(x => x.ParentId == 1).ToList();
    var parent = context.Parents.AsNoTracking().Single(x => x.ParentId == 1);
    var count = parent.Children.Count;  // will still return 0.
}

因此,如果您意外地看到其他东西并且延迟加载肯定被禁用并确认不会发生(在探查器中没有观察到查询)那么我怀疑您有其他事情在起作用。

此代码例如发出红旗:

public async Task<IQueryable<Customer>> GetAll(CancellationToken cancellationToken)
{
    int minAccessRight = //...
    int currentUserId = //...

    return from acl in (from acl in this.aclSet
                   where acl.UserId == currentUserId &&
                         acl.AccessRight >= minAccessRight
                   select acl)
    select acl.Entity;
}

IQueryable 返回方法不需要声明为 async 并且可能不应该声明,因为这可能会触发实际执行的 tracking-like 查询或其他一些奇怪的行为。相反,尝试:

public IQueryable<Customer> GetAll(CancellationToken cancellationToken)
{
    int minAccessRight = //...
    int currentUserId = //...

    var query = (from acl in this.aclSet
                   where acl.UserId == currentUserId &&
                         acl.AccessRight >= minAccessRight
                   select acl);
    return query;
}

然后在调用中:

IQueryable<Customer> query = Repository.GetAll(cancellationToken)
    .Include(e => e.Partners)
    .Where(e => e.Id == entityId)
    .AsNoTracking();

return await query.FirstOrDefaultAsync(cancellationToken);

查询的执行仍然是异步的,这就是 async 重要的地方。查询组合可能需要异步的唯一原因是,如果在构建该查询时导致一些特别昂贵的检查等,这可能会受益于 async,但是我可能会委托它退出并具有这些值 pre-fetched 异步并将值作为参数传递。 (即,如果将 minAccessRight/currentUserId 等移交给异步调用,加载这些 ID 并将它们作为参数传递)