实体框架核心。更新数据库中的数据。跟踪数据时方法无法正常工作

EntityFrameworkCore. Update data in database. Method does not work correctly while data is being tracked

我在创建项目时发现了一个问题。如果有人提到这个问题,我将不胜感激。 在我的项目中,我使用分层模型。与数据库(DB)通信的存储层(数据访问层)和实现服务和对象(数据传输对象)的服务层(业务逻辑层)。

因此,dbSet.Update方法有问题。当对象 (obj) 作为参数进入 Update 方法时,在 _db.Entry(dbSet.Local.FirstOrDefault(i => i.Id == obj.Id) ?? obj) 的方法调用期间.State = EntityState.Modified or _db.Update(dbSet.Local.FirstOrDefault(i => i.Id == obj.Id) ?? obj) 在第一个用户更新的情况下( like from "view.xaml") obj 更新并更改保存在数据库中(因为 dbSet.Local.FirstOrDefault(i => i.Id == obj.Id) returns null and显然我的对象进入了 _db.Update 方法)。如果重复用户更新(在“view.xaml”视图中)对象 obj -- 当它进入 _db.Update(dbSet.Local.FirstOrDefault(i => i.Id == obj.Id) ?? obj) 方法,它没有被考虑,因为数据上下文已经跟踪它并且在 _db.Update 方法中从 dbSet.Local.FirstOrDefault(i => i.Id == obj.Id)。 如果根据来自用户的类型更新 dbSet.Local 中的这个对象,一切都会好起来的。但是,情况并非如此,当用户编辑其属性时,它会被跟踪但不会更改。它没有被正确跟踪,而是因为我使用服务,因此,数据传输对象实体。

鉴于以上情况,我有一个问题。 如何更新被跟踪的实体(通过新修改的对象),或者如何手动分配 dbSet.Local 中的必要对象以替换存储在那里的对象?或者如何使 Microsoft.EntityFrameworkCore.ChangeTracking 不以任何方式跟踪对我的对象的更改?

为了使更改不被跟踪,我为 DbContextOptionsBuilder 实体使用了 QueryTrackingBehavior.NoTracking 参数,但这只对第一次加载有帮助,当数据进一步更新时仍然使用跟踪。 我还使用了 dbSet.Local.Clear() 方法,但这是一个坏主意,因为由于从数据库中删除了以前的数据而导致数据更新(例如从 table 中删除 20 行并添加一个已更新)。

public abstract class GenericRepository<T, TKey> : IGenericRepository<T, TKey> where T : class, IEntity, new()
{
    protected DbContext context;
    protected DbSet<T> dbSet;

    private readonly object _lock = new object();

    public GenericRepository(DbContext context)
    {
        this.context = context;
        this.dbSet = context.Set<T>();
    }
    public virtual IQueryable<T> GetAll()
    {
        lock (_lock)
            return dbSet;
    }
    public virtual async Task<T> GetAsync(TKey id)
    {
        try
        {
            return await dbSet.FindAsync(id);
        }
        catch (Exception exc)
        {
            throw exc;
        }
    }
    public async Task AddAsync(T obj)
    {
        try
        {
            //dbSet.Local.Clear();
            await dbSet.AddAsync(obj);
            //context.Entry(obj).State = EntityState.Added;
            
            
        }
        catch (Exception exc)
        {
            throw exc;
        }
    }
    public async void Delete(TKey id)
    {
        //context.Entry(obj).State = EntityState.Deleted;
        //context.Remove(obj);
        try
        {
            T obj = await GetAsync(id);
            dbSet.Remove(obj);
            //dbSet.Remove(dbSet.Find(1504));
        }
        catch (Exception exc)
        {
            throw exc;
        }
    }
    public void Update(T obj)
    {
        try
        {
            
            context.Entry(dbSet.Local.FirstOrDefault(i => i.Id == obj.Id) ?? obj).State = EntityState.Modified;
            //dbSet.Update(dbSet.Local.FirstOrDefault(i => i.Id == obj.Id) ?? obj);

            //dbSet.Update(obj).State = EntityState.Modified;
            //dbSet.Update(obj);
            //dbSet.Local.FirstOrDefault(i => i.Id == obj.Id)
        }
        catch (Exception exc) { throw exc; }
    }
    public async Task SaveAsync()
    {
        try
        {
            await context.SaveChangesAsync();
        }
        catch (Exception exc)
        {
            throw exc;
        }
    }
    public virtual IQueryable<T> Where(Expression<Func<T, bool>> predicate)
    {
        lock (_lock)
            return dbSet.Where(predicate);
    }
    //public virtual IQueryable<T> FindBy(Expression<Func<T, bool>> predicate)
    //{
    //    return dbSet.Where(predicate);
    //}

}

}

数据保存方法发生在更高级别(服务级别)。在 View 中用户更新数据(例如,调整 属性 DataGrid 的其中一行),然后在绑定到此 View 的上下文中,特别是在 ViewModel 中,调用绑定 属性(通过INotifyPropertyChanged),它发起调用负责CRUD操作的方法,然后该方法(On_bt_CategoryUpdate_Command)从负责更新数据库中数据的genericService的私有字段调用方法。

public class MainViewModel : ViewModel
{

    #region Fields
    private IGenericService<ContractDTO, int> _contractService;
    
    #endregion

    #region Commands
    #endregion

    #region Properties
    private object GetList;                  
    public object _GetList { get => GetList; private set => Set(ref GetList, value); }     //Property to initialize DataGrid

    private ContractDTO _contractDTO;
    public ContractDTO SelectedItem { get => _contractDTO; set => Set(ref _contractDTO, value); }         //Property with the data of the selected row in the DataGrid

    #endregion

    public SpesTechViewModel(IGenericService<ContractDTO, int> contractService)
    {
        _contractService = contractService;
    }

    #region Commands function
    
    private bool Can_bt_CategoryUpdate_Command() => true;

    private  void On_bt_CategoryUpdate_Command()
    {
        try
        {
            _contractService.UpdateAsync(SelectedItem);   //Method that updates and saves the data in the database
        }
        catch (Exception exc) { MessageBox.Show(exc.Message); }
    }
    #endregion

    #region Function
    
    void GetByFilter<T>(IFilterModel<T> filter, IGenericService<T, int> service) where T : class, new()
    {
        lock (_lock)
            _GetList = service.Where(filter.Predicate()).Result.ToObservableCollection();

    }
    
    #endregion
}

_contractService.UpdateAsync(SelectedItem):

public async Task<DbObjectDTO> UpdateAsync(DbObjectDTO obj)
    {
        try
        {
            DbObject dbObject = mapper.Map<DbObject>(obj);
            repository.Update(dbObject);
            await repository.SaveAsync();
            return mapper.Map<DbObjectDTO>(dbObject);
        }
        catch (Exception exc) { throw exc; }
    }

本方法第一件事:使用AutoMapper,将DTO对象转换为DAL对象; 第二:使用存储库层更新数据(上面的代码,public void Update(T obj) 方法);第三:保存到数据库;第四:反向映射。

如果我在 GenericRepository 的 Update(T obj) 方法中尝试这样做:

public void Update(T obj)
    {
        try
        {
            dbSet.Update(obj);

            //dbSet.Local.FirstOrDefault(i => i.Id == obj.Id)
        }
        catch (Exception exc) { throw exc; }
    }

在对 dbSet 中的同一实体进行第二次更新时,对 T obj 进行了新的更改,我得到一个异常:

{“无法跟踪实体类型 'Contract' 的实例,因为已跟踪另一个具有与 {'Id'} 相同键值的实例。附加现有实体时,确保只有一个附加了具有给定键值的实体实例。考虑使用 'DbContextOptionsBuilder.EnableSensitiveDataLogging' 查看冲突的键值。"}

与本地缓存如此紧密地耦合是不寻常的。正常模式是调用 DbSet.Find(id),并更新它 returns.

的实体的属性
var entity = await dbSet.FindAsync(new object[] { id }, cancellationToken);
var entry = context.Entry(entity);
entry.CurrentValues.SetValues(obj);
await context.SaveChangesAsync(cancellationToken);

与您正在做的相比,此模式有一些好处:

  • 如果不存在具有该 ID 的实体,您有机会通知调用者
  • Entity Framework 处理缓存
  • Entity Framework只更新实际改变的属性

如果实体不在缓存中,那么 Entity Framework 必须转到数据库。在某些情况下,例如如果您的实体有一个很大的二进制文件 属性 并且您打算用新值覆盖它,您可能希望避免从数据库加载现有值并直接跳到更新部分。

var local = dbSet.Local.FirstOrDefault(i => i.Id == obj.Id);
if (local != null)
{
    var entry = context.Entry(local);
    entry.CurrentValues.SetValues(obj);
}
else
{
    var entry = dbSet.Attach(obj);
    entry.State = EntityState.Modified;
}

await context.SaveChangesAsync(cancellationToken);

这是一般用例的一个例外,我建议您在大多数情况下遵循正常模式,并且仅在性能要求时才使用它。