尝试更新数据库时出现以下错误:
System.InvalidOperationException:“无法跟踪实体类型“Expense”的实例,因为已经在跟踪具有相同键值 {'Id'} 的另一个实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。考虑使用“DbContextOptionsBuilder.EnableSensitiveDataLogging”来查看冲突的键值。”
我认为我缺少一些基本的理解。尝试用谷歌搜索这个错误主要给了我将服务从单例更改为作用域的答案,但我的服务是作用域的。这是相关代码:
private async void SaveExpensesToDatabase()
{
foreach(Expense expense in expenses)
{
expense.CategoryName = expense.Category.Name;
Expense check = await expenseService.GetExpensebyKey(expense.Id);
if(check != null)
{
await expenseService.UpdateExpenseDetails(expense);
}
}
}
以下是expenseService中的两个函数:
public async Task<Expense> GetExpensebyKey(string Id)
{
Expense expense = await _dbContext.Expenses.FirstOrDefaultAsync(x => x.Id == Id);
return expense;
}
public async Task<bool> UpdateExpenseDetails(Expense expense)
{
_dbContext.Expenses.Update(expense);
await _dbContext.SaveChangesAsync();
return true;
}
这就是我在程序文件中列出服务的方式:
builder.Services.AddScoped<ExpenseService>();
我不只是在寻找如何使这项工作成功,尽管那会很棒。我也在寻找我不理解的基本概念。我认为它以数据库上下文的工作方式为中心。预先感谢!
在这一行中,您将通过“Expense”集合进行交互。
foreach(Expense expense in expenses)
您几乎可以肯定通过对 DbContext 执行类似的操作来获得这一点。
var expenses = _dbContext.Expenses.Where(....);
因此,此时 DbContext 正在跟踪您检索到的所有实体。
然后你这样做:
Expense check = await expenseService.GetExpensebyKey(expense.Id);
因此您现在正在跟踪同一实体的两个副本。
然后你这样做:
_dbContext.Expenses.Update(expense);
告诉它它几乎肯定已经知道的更改:它正在从原始列表查询中跟踪
expense
实例。
并尝试去做:
await _dbContext.SaveChangesAsync();
EF现在很困惑。它在不同的州至少有两个具有相同 Id 的实体。它持久化回数据库中的哪一个。不知道,所以会抛出错误。
在 Blazor 中,正常的解决方案是使用从 DbContextFactory 获取的每个事务 DbContext。这也克服了尝试针对单个 DbContext 运行异步操作的问题。
此处解释了如何设置传真工厂 - https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/#using-a-dbcontext-factory-eg-for-blazor
在许多 Stackoverflow 答案中:
Blazor 服务器:将 EF Core DbContextFactory 与 DbContext 混合 AddDbContext 和 AddDbContextFactory 的区别
所以你的服务类看起来像这样:
private readonly IDbContextFactory<MyDbContext> _factory;
public MyService(IDbContextFactory<MyDbContext> factory)
{
_factory = factory;
}
及方法:
public async Task<bool> UpdateExpenseDetails(Expense expense)
{
// create a DbContext scoped to the method
using var dbContext = _factory.CreateDbContext();
//check if the record exists
var exists = dbcontext.Expenses.Any(x => x.Id == expense.Id));
//Exit it it doesn't.
// ?? Don't you need to do a add here?
if (!exist)
return false;
// pass the modified expense into the DbContext
dbContext.Expenses.Update(expense);
// commit the transaction
var transactions = await _dbContext.SaveChangesAsync();
// check we did 1 update
// You should consider returning some sort of result object with status and a mnesage if failed
return transactions == 1 ? true: false;
}
您的列表查询可能如下所示。我特意分解了该查询,以展示如何在实际[具体化]执行它之前构建多部分查询
ToListAsync()
。 EF 将构建查询并仅获取从服务器请求的数据。
using var dbContext = _factory.CreateDbContext();
// turn off tracking as we are only doing queries during this transaction
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
IQueryable<Expernse> query = dbContext.Set<TRecord>();
query = query.Where(your query here);
// example of adding paging to a query
// if (request.PageSize > 0)
// query = query
// .Skip(request.StartIndex)
// .Take(request.PageSize);
var list = query is IAsyncEnumerable<TRecord>
? await query.ToListAsync()
: query.ToList();