并发错误导致“数据库操作预计影响 1 行,但实际影响 0 行;”

问题描述 投票:0回答:2

我有以下函数,它接受

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
    }
c# asp.net entity-framework entity-framework-core
2个回答
0
投票

如果您添加新的突出显示,直接更新_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;
}

0
投票

这种方法存在一些问题。主要是尝试通过创建新集合来替换子集合引用:

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()
来解决生成的单个查询中存在大型笛卡尔积的可能性。

© www.soinside.com 2019 - 2024. All rights reserved.