如何强制实体框架始终从数据库中获取更新数据?

Posted

技术标签:

【中文标题】如何强制实体框架始终从数据库中获取更新数据?【英文标题】:How to force Entity Framework to always get updated data from the database? 【发布时间】:2014-04-06 07:33:32 【问题描述】:

我正在使用EntityFramework.Extended 库来执行批量更新。唯一的问题是 EF 不跟踪库执行的批量更新。因此,当我再次查询 DbContext 时,它不会返回更新的实体。

我发现在查询时使用AsNoTracking() 方法会禁用跟踪并从数据库中获取新数据。但是,由于 EF 不跟踪使用 AsNoTracking() 查询的实体,我无法对查询的数据执行任何更新。

有什么方法可以强制 EF 在跟踪更改的同时获取最新数据?

【问题讨论】:

29k 浏览量,只有 19 人对此表示赞同...好吧,我还是添加了我的 【参考方案1】:

请尝试刷新单个实体:

Context.Entry<T>(entity).Reload()

编辑: 要获取实体集合的最新数据,值得尝试在每次请求后处理 DbContext 实例。

【讨论】:

谢谢!有没有办法重新加载实体集合而不是一个?最好是整个DbSet 您还需要其他什么才能将其视为答案吗? 是的,它仍然没有显示如何重新加载实体集合。 您是否尝试过处理您的 dbcontext 并创建一个新的? 我花了几个小时才得出这个结论。答案中的 EDIT 帮助我找到了解决方案。谢谢!【参考方案2】:

我在搜索更新实体后导航属性未填充的问题的解决方案时偶然发现了这个问题。每当我尝试从数据库重新加载实体时,它都会从本地存储中获取条目,而不是通过延迟加载来填充导航属性。我发现这不是破坏上下文并重新创建上下文,而是让我在代理工作的情况下获得新数据:

_db.Entry(entity).State = EntityState.Detached;

其背后的逻辑是 - 我的更新附加了实体,以便跟踪对其的更改。这会将其添加到本地商店。此后,任何使用功能代理检索实体的尝试都将导致它获取本地代理,而不是转到数据库并返回一个新的启用代理的实体。我尝试了上面的重新加载选项,它确实从数据库中刷新了对象,但这并没有为您提供延迟加载的代理对象。我试着做一个Find(id), Where(t =&gt; t.Id = id), First(t =&gt; t.Id = id)。最后,我检查了提供的可用状态,发现有一个“分离”状态。尤里卡!希望这对某人有所帮助。

【讨论】:

请问你在哪里应用 Detached 状态来让延迟加载工作?我尝试了您的解决方案,但它对我不起作用,我一定错过了一些东西。您的帮助将不胜感激 我想通了:1. 保存实体,2. 将状态设置为分离,3. 从数据库中读取实体。感谢您的提示!【参考方案3】:

我将实体变量声明为类的一部分,但没有赋值。这使我可以在不丢失变量以供其他方法引用的情况下处理实例。我刚遇到这个,所以它没有太多的运行时间,但到目前为止它似乎工作正常。

public partial class frmMyForm

    private My_Entities entity;

    public frmMyForm()
    
        InitializeComponent();
    

    private void SomeControl_Click(object sender, EventArgs e)
    
        db.SaveChanges();
        db.Dispose();
        entity = new My_Entities();
        //more code using entity ...

【讨论】:

【参考方案4】:

使代码在同一上下文中运行不会产生更新的实体。它只会在运行之间附加在数据库中创建的新实体。 EF 强制重新加载可以这样完成:

ObjectQuery _query = Entity.MyEntity;
_query.MergeOption = MergeOption.OverwriteChanges;
var myEntity = _query.Where(x => x.Id > 0).ToList();

【讨论】:

【参考方案5】:

偶然发现了这个问题。我的应用没有从数据库返回新数据。

这些似乎是 3 个解决方案:

    选择时重新加载:首先选择对象,然后重新加载。如果没有缓存,加载两次?

    使用后分离:如果您忘记在使用后分离一个对象,这将导致应用程序的完全独立部分出现错误,并且极难追踪。

    在使用后处理 DbContext。绝对是要走的路。

我在 Repository 类中创建了我的 DbContext 实例。如果 DbContext 在存储库级别声明,那么我无法控制它的处置方式。那是不行的。如果我在每次调用时都创建一个新的 DbContext,那么我无法调用 Select、修改数据,然后调用 Update。

似乎我的存储库模式中根本缺少某些东西。

在对基本的存储库模式进行了一些研究后,我找到了解决方案:工作单元模式和存储库模式。

This is an excellent article on the Unit of Work pattern

或this article from Microsoft。我目前拥有的是页面上方的存储库,缺少的是“实现通用存储库和工作单元类”部分

基本上,您不是将存储库注入您的服务,而是通过注入服务的 UnitOfWork 访问所有存储库。它会解决很多问题。

public class UnitOfWork : IUnitOfWork

    private readonly ApplicationContext _context;
    public UnitOfWork(ApplicationContext context)
    
        _context = context;
        Developers = new DeveloperRepository(_context);
        Projects = new ProjectRepository(_context);
    
    public IDeveloperRepository Developers  get; private set; 
    public IProjectRepository Projects  get; private set; 
    public int Complete()
    
        return _context.SaveChanges();
    
    public void Dispose()
    
        _context.Dispose();
    

剩下的问题是:如何创建 IUnitOfWork 实例?

如果我在类构造函数中创建它以像存储库一样注入它,那么它会以完全相同的方式创建和销毁,我们又回到了同样的问题。在 ASP.NET 和 MVC 中,类实例的生命周期很短,因此在构造函数中注入可能没问题,但在 Blazor 和桌面应用程序中,类实例的生命周期要长得多,而且更成问题。

This article from Microsoft 明确指出依赖注入不适合在 Blazor 中管理 DbContext 的生命周期:

在 Blazor Server 应用中,范围服务注册可能会出现问题 因为该实例是在用户的组件之间共享的 电路。 DbContext 不是线程安全的,也不是为并发设计的 采用。由于以下原因,现有的生命周期是不合适的:

Singleton 在应用程序的所有用户之间共享状态并导致 不适当的同时使用。 Scoped(默认)构成类似 同一用户的组件之间的问题。 瞬态导致新的 每个请求的实例;但由于组件可以长期使用,这 导致上下文的寿命比预期的要长。

他们建议使用工厂模式,可以这样实现

/// <summary>
/// Creates instances of UnitOfWork. Repositories and UnitOfWork are not automatically injected through dependency injection,
/// and this class is the only one injected into classes to give access to the rest.
/// </summary>
public class UnitOfWorkFactory : IUnitOfWorkFactory

    private readonly IDateTimeService _dateService;
    private readonly DbContextOptions<PaymentsContext> _options;

    public UnitOfWorkFactory(IDateTimeService dateService, DbContextOptions<PaymentsContext> options)
    
        _dateService = dateService;
        _options = options;
    

    /// <summary>
    /// Creates a new Unit of Work, which can be viewed as a transaction. It provides access to all data repositories.
    /// </summary>
    /// <returns>The new Unit of Work.</returns>
    public IUnitOfWork Create() => new UnitOfWork(CreateContext(), _dateService);

    /// <summary>
    /// Creates a new DbContext.
    /// </summary>
    /// <returns>The new DbContext.</returns>
    public PaymentsContext CreateContext() => new(_options);

IWorkOfUnit 和任何存储库都不会注册到 IoC 容器中。只有 IWorkOfUnitFactory。

最后...如何在各种服务之间共享事务?

我有一个 SetStatus 方法来更新数据库中的状态字段。这种方法应该如何知道它是独立操作还是更大事务的一部分?

由于类级别的依赖注入不适合管理和共享单元的工作,那么唯一的选择就是将其作为参数传递给需要它的方法。

我为每个需要它的方法添加一个可选的IUnitOfWork? workScope = null 参数,并且仅当此参数为空时才调用 Save。这是一个实现。

public virtual async Task<TempOrder?> SetStatusAsync(int orderId, PaymentStatus status, IUnitOfWork? workScope = null)

    using var unitOfWork = _workFactory.Create();
    var work = workScope ?? unitOfWork;

    var order = await work.Orders.GetByIdAsync(orderId);
    if (order != null)
    
        order.Status = status;
        work.Orders.Update(order); // DateModified gets set here

        if (workScope == null)
        
            await work.SaveAsync();
        
    
    return order;

另一种选择是让 IUnitOfWorkFactory.Create 采用 workScope 参数,并在设置时:

重用现有的 DbContext 请勿丢弃 IUnitOfWork.Save 不会提交

我的最终实现可以这样使用

public virtual async Task<TempOrder?> SetStatusAsync(int orderId, PaymentStatus status, IUnitOfWork? workScope = null)

    using var unitOfWork = _workFactory.Create(workScope);

    var order = await unitOfWork.Orders.GetByIdAsync(orderId);
    if (order != null)
    
        order.Status = status;
        work.Orders.Update(order); // DateModified gets set here
        await unitOfWork.SaveAsync(); // Ignored if workScope != null
    
    return order;

呸!那个虫子是一个兔子洞。这是一个相当长的解决方案,但应该通过可靠的架构来解决它。

【讨论】:

【参考方案6】:

对我来说... 我像这样访问我的 DbContext:

_viewModel.Repo.Context

为了强制 EF 访问数据库,我这样做:

_viewModel.Repo.Context = new NewDispatchContext();

用新实例覆盖当前 DbContext。然后下次我使用我的数据服务时,他们会从数据库中获取数据。

【讨论】:

你应该处理之前的上下文。【参考方案7】:

重新加载特定实体对我来说不是一个选项,因为我不知道确切的实体。我也不想创建一个新的 DbContext,因为它是由 DI 注入的。所以我采用了以下技巧来“重置”整个上下文。

            foreach (var entry in db.ChangeTracker.Entries())
            
                entry.State = EntityState.Detached;
            

【讨论】:

对于真实的代码,hack 只能弥补糟糕的架构,但这段代码对于单元测试非常有用。用数据填充内存数据库后,最好在测试前重置跟踪器以使其处于“干净”状态。

以上是关于如何强制实体框架始终从数据库中获取更新数据?的主要内容,如果未能解决你的问题,请参考以下文章

实体框架代码优先:迁移失败并更新数据库,强制不必要的(?)添加迁移

如何让实体框架在不保存对象的情况下创建/更新数据库表

如何使用实体框架从 DataGrid 更新数据库

实体框架:开发不执行更新和删除的模型

如何使用实体框架更新数据库中的记录时覆盖模型验证

从一个 id 更新实体框架中的多行