我有以下函数,它接受
BookForUpdateDto
并根据更新的属性更新 Book
实体。当我尝试更新 Book
的 ICollection 内部的项目时,在添加一个项目或删除一个项目时,它可以正常工作,但是当在另一个项目之后调用这些操作时(第一个调用,其中删除了集合中的一个项目,然后在添加另一项的情况下进行一次调用),我在 SaveChangesAsync()
上收到异常,告诉我“数据库操作预计会影响 1 行,但实际上影响了 0 行;”。
这是我的代码:
public async Task UpdateBookAsync(string email, BookForUpdateDto bookUpdateDto)
{
var user = await _userRepository.GetAsync(email, trackChanges: true);
var book = user.Books.SingleOrDefault(book => book.BookId == bookUpdateDto.Guid);
if (book == null)
{
const string message = "No book with this id exists";
throw new CommonErrorException(404, message, 4);
}
await _bookRepository.LoadRelationShipsAsync(book);
var dtoProperties = bookUpdateDto.GetType().GetProperties();
foreach (var dtoProperty in dtoProperties)
{
// Manually handle certain properties
switch (dtoProperty.Name)
{
// ...
case "Highlights":
{
Collection<Highlight> newHighlights = new();
foreach(var highlightInDto in bookUpdateDto.Highlights)
{
newHighlights.Add(_mapper.Map<Highlight>(highlightInDto));
}
book.Highlights = newHighlights;
continue;
}
}
// Update any other property via reflection
var value = dtoProperty.GetValue(bookUpdateDto);
SetPropertyOnBook(book, dtoProperty.Name, value);
}
await _bookRepository.SaveChangesAsync(); // <-- Error is thrown here
}
如果您添加新的突出显示,直接更新_bookRepository.Highlights表(而不是书籍集合),您可以添加每个新行,而不必担心乐观并发。你可以这样做:
...
case "Highlights":
{
Collection<Highlight> newHighlights = new();
foreach(var highlightInDto in bookUpdateDto.Highlights)
{
newHighlights.Add(_mapper.Map<Highlight>(highlightInDto));
}
foreach (Highlight hl in newHighlights)
{
hl.BookId = book.BookId;
_bookRepository.Highlights.Add(hl);
}
_bookRepository.SaveChanges();
continue;
}
这种方法存在一些问题。主要是尝试通过创建新集合来替换子集合引用:
foreach(var highlightInDto in bookUpdateDto.Highlights)
{
newHighlights.Add(_mapper.Map<Highlight>(highlightInDto));
}
book.Highlights = newHighlights;
使用单数引用,您可以将项目替换为 DbContext 跟踪的实例,使用集合,您需要添加和删除引用。
例如,如果您的书有一组突出显示,并且您想用一组新值替换这些值,则需要确定需要添加哪些突出显示,以及可能需要替换哪些突出显示。如果突出显示是对表中现有记录的引用,而不是每本书“拥有”的唯一子项,那么您需要加载对要从 DbContext 添加的突出显示的引用,并将它们与书籍关联。
为了解决更新书中的要点,假设要点是参考:
var updatedHighlightIds = bookUpdateDto.Highlights
.Select(x => x.HighlightId);
var existingHighlightIds = book.Highlights
.Select(x => x.HighlightId);
var highlightIdsToAdd = updatedHighlightIds
.Except(existingHighlightIds)
.ToList();
var highlightIdsToRemove = existingHighlightIds
.Except(updatedHighlightIds)
.ToList();
foreach(var highlightId in highlightIdsToRemove)
{
book.Highlights.Remove(book.Highlights.First(x => x.HighlightId == highlightId));
}
if (highlightIdsToAdd.Any())
{
var highlights = await _context.Highlights
.Where(x => highlightIdsToAdd.Contains(x.HighlightId))
.ToListAsync();
foreach(var highlight in highlights)
{
book.Highlights.Add(highlight);
}
}
如果突出显示是作为新参考创建的,由每本书单独拥有,那么您可以使用类似的方法,但不是从 DbContext 获取突出显示,而是向上
new
需要添加的突出显示。在这些情况下,高亮显示的匹配可能不是基于高亮显示 ID 完成的,而是基于唯一标识它们的东西来完成。
我不建议使用每个实体的存储库模式,例如带有 EF 的通用存储库。在使用 EF 时,此类模式不会增加任何实际价值,并且会对您的操作施加重大限制。目前还不清楚这个方法的作用:
await _bookRepository.LoadRelationShipsAsync(book);
但我假设它将显式加载给定书籍的亮点和其他关系参考。
代码如下:
var user = await _userRepository.GetAsync(email, trackChanges: true);
var book = user.Books.SingleOrDefault(book => book.BookId == bookUpdateDto.Guid);
特别浪费,因为您正在加载用户,然后延迟加载该用户的所有书籍。或者,使用 DbContext/DbSet 和导航属性,可以将其替换为:
var book = await _context.Books
.Include(x => x.Highlights)
.SingleOrDefaultAsync(x => x.BookId == bookUpdateDto.Guid && x.User.Email == email);
这将仅获取一本书及其在一次查询命中中的亮点。如果您需要包含多个关系,您可以附加
.AsSplitQuery()
来解决生成的单个查询中存在大型笛卡尔积的可能性。