DbSet<>.Local需要特别小心使用吗?

19
几天来,我一直在尝试从存储库(DbContext)中检索实体。我试图将所有的实体都保存在一个原子操作中。因此,不同的实体共同代表了对我有价值的东西。如果所有实体都是“有效的”,那么我就可以将它们全部保存到数据库中。实体'a'已经存储在我的存储库中,并且需要检索以“验证”实体'b'。这就是问题所在。我的存储库依赖于DbSet<TEntity>类,它与Linq2Sql非常配合(如Include()导航属性等)。但是,DbSet<TEntity>不包含处于“已添加”状态的实体。因此,我们有两个选择:1.使用ChangeTracker查看可用的实体并根据它们的EntityState将它们查询到集合中;2.使用DbSet<TEntity>.Local属性。
“ChangeTracker”似乎需要额外的努力才能使其以一种可以使用Linq2Sql的方式工作,例如Include()导航属性。
对我来说,“DbSet<TEntity>.Local”有点奇怪。可能只是名称问题。我刚刚读到了一些内容,它的性能不太好(比DbSet<>本身慢)。不确定这是否是错误的陈述。
有经验的EntityFramework用户可以为此提供一些见解吗?应该遵循什么“明智”的路径?或者我是在看幽灵,应该始终使用.Local属性?
带有代码示例的更新:
一个出错的例子
    public void AddAndRetrieveUncommittedTenant()
    {
        _tenantRepository = new TenantRepository(new TenantApplicationTestContext());

        const string tenantName = "testtenant";

        // Create the tenant, but not call `SaveChanges` yet until all entities are validated 
        _tenantRepository.Create(tenantName);

        //
        // Some other code
        //

        var tenant = _tenantRepository.GetTenants().FirstOrDefault(entity => entity.Name.Equals(tenantName));

        // The tenant will be null, because I did not call save changes yet,
        // and the implementation of the Repository uses a DbSet<TEntity>
        // instead of the DbSet<TEntity>.Local.
        Assert.IsNotNull(tenant);

        // Can I safely use DbSet<TEntity>.Local ? Or should I play 
        // around with DbContext.ChangeTracker instead?
    }

我想展示如何使用我的 Repository

在我的 Repository 中,我有这个方法:

    public IQueryable<TEntity> GetAll()
    {
        return Context.Set<TEntity>().AsQueryable();
    }

我在商业代码中以这种方式使用:
    public List<Case> GetCasesForUser(User user)
    {
        return _repository.GetAll().
            Where(@case => @case.Owner.EmailAddress.Equals(user.EmailAddress)).
            Include(@case => @case.Type).
            Include(@case => @case.Owner).
            ToList();
    }

这主要是我喜欢使用类似于DbSet的变量的原因。我需要灵活地Include导航属性。如果我使用ChangeTracker,则会在List中检索实体,这不允许我在以后的某个时间点上惰性加载相关实体。
如果这接近难以理解的胡说八道,请告诉我,以便我可以改进问题。我迫切需要一个答案。
非常感谢!

即使您可能不相信它提供了价值,提供代码仍然可以为试图回答问题的人提供上下文。您能否添加一个链接,指向您“读到某些东西表现不佳”的地方? - Ryan Gates
你能否给我们更多关于你想要实现的细节呢?同时提供一些带有验证的代码示例将会很有用。 - Kirill Bestemyanov
我将会在问题中更新一些代码示例。这里有一个关于性能的帖子(https://dev59.com/4Gct5IYBdhLWcg3wQ7Qy)。我理解您对此并不是很确定。 - bas
第一个修复方法可以是使Create方法返回新实体。 - Gert Arnold
4个回答

16
如果您希望能够轻松地针对DbSet发出查询,并在其中查找新创建的项目,则需要在每个实体创建后调用SaveChanges()。如果您正在使用“工作单元”风格的方法来处理持久实体,这其实并不会有问题,因为您可以让工作单元将所有操作包装在一个DB事务中(即在创建UoW时创建新的TransactionScope,并在UoW完成时调用Commit())。有了这种结构,更改会被发送到DB,并且对于DbSet是可见的,但对于其他UoW是不可见的(除非您使用的隔离级别)。
如果您不想要这种额外负担,那么您需要修改您的代码,在适当的时间使用Local(这可能需要查看Local,然后在DbSet上发出查询,如果没有找到您要寻找的内容)。在这些情况下,DbSet上的Find()方法也非常有帮助。它可以在Local或DB中通过主键查找实体。因此,如果您只需要按主键定位项目,这非常方便(并且还具有性能优势)。

7

正如Terry Coatta提到的那样,如果你不想先保存记录,最好的方法是检查两个来源。

例如:

public Person LookupPerson(string emailAddress, DateTime effectiveDate)
{
    Expression<Func<Person, bool>> criteria = 
        p =>
            p.EmailAddress == emailAddress &&
            p.EffectiveDate == effectiveDate;

    return LookupPerson(_context.ObjectSet<Person>.Local.AsQueryable(), criteria) ?? // Search local
           LookupPerson(_context.ObjectSet<Person>.AsQueryable(), criteria); // Search database
}

private Person LookupPerson(IQueryable<Person> source, Expression<Func<Person, bool>> predicate)
{
    return source.FirstOrDefault(predicate);
}

1

这可能只适用于EF Core,但每次引用DbSet的.Local时,都会在上下文中静默触发更改检测,这可能会影响性能,具体取决于您的模型有多复杂以及当前正在跟踪多少条目。

如果这是一个问题,您需要使用(对于EFCore)dbContext.ChangeTracker.Entries<T>()来获取本地跟踪的实体,这不会触发更改检测,但需要手动过滤DB状态,因为它将包括已删除和分离的实体。

在EF6中也有类似的版本,但在EFCore中,Entries是EntityEntries列表,您需要选择出entry.Entity以获取与DbSet相同的数据。


0
对于后来者,我遇到了一些类似的问题,并决定尝试使用.Concat方法。我没有进行广泛的性能测试,因此比我更有知识的人应该随时发表意见。
基本上,为了将功能正确地分解成较小的块,我最终得到了这样一种情况:我有一个方法不知道当前UoW中对该方法的连续或先前调用。所以我做了这个:
var context = new MyDbContextClass();
var emp = context.Employees.Concat(context.Employees.Local).FirstOrDefault(e => e.Name.Contains("some name"));

看起来这是一个不错的简洁且不太显眼的解决方法。不幸的是,对我并没有起作用。我的谓词有点复杂,我调用了 Where() 而不是 FirstOrDefault()。当访问结果集时,我得到了以下错误:系统不支持的异常类型“System.NotSupportedException”出现在 EntityFramework.dll 中,但未在用户代码中处理。其他信息:无法创建类型为“Employee”的常量值。只支持基本类型或枚举类型。 因此我选择分别查询两个集合。 - AronVanAmmers
不是很确定,但我认为在某些情况下这可能会严重降低性能,因为它将从数据库中提取所有员工,将它们转换为对象,然后对它们运行查询。你(我们)真的需要一个系统来执行以下操作: context.Employees.Where(predicate).Concat(context.Employees.Local.Where(predictae)) - C.List

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接