实体框架核心。更新数据库中的数据。跟踪数据时方法无法正常工作
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);
这是一般用例的一个例外,我建议您在大多数情况下遵循正常模式,并且仅在性能要求时才使用它。
我在创建项目时发现了一个问题。如果有人提到这个问题,我将不胜感激。 在我的项目中,我使用分层模型。与数据库(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
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);
这是一般用例的一个例外,我建议您在大多数情况下遵循正常模式,并且仅在性能要求时才使用它。