首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >更新自切换到DbContextFactory后不起作用

更新自切换到DbContextFactory后不起作用
EN

Stack Overflow用户
提问于 2022-01-15 16:34:32
回答 1查看 594关注 0票数 0

我最近换了一个ContextFactory,因为second operation was started on this context ...

所以我注册了DbContextFactory:

代码语言:javascript
复制
builder.Services.AddDbContextFactory<CharacterSheetDbContext>(
    options => {
        options.UseMySql(
            builder.Configuration.GetConnectionString("DefaultConnection"),
            new MySqlServerVersion(new Version(8, 0, 27))
        );
        options.EnableSensitiveDataLogging();
    }
);

然后我有了我的服务层:

当工厂被注入发动机时:

代码语言:javascript
复制
public ARepository(IDbContextFactory<CharacterSheetDbContext> contextFactory) {
        _contextFactory = contextFactory;
    }

和更新方法(在这里我创建上下文):

代码语言:javascript
复制
public async Task UpdateAsync(TEntity entity) {
        await using var context = await _contextFactory.CreateDbContextAsync();
        var set = context.Set<TEntity>();
        //_context.ChangeTracker.Clear(); <-- (tried with and without this one)
        set.Update(entity);
        await context.SaveChangesAsync();
    }

但是,当我对一个实体调用Update时,它会抛出以下错误:

代码语言:javascript
复制
An exception occurred in the database while saving changes for context type 'Model.Configurations.CharacterSheetDbContext'.
      Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
       ---> MySqlConnector.MySqlException (0x80004005): Duplicate entry '3-9' for key 'characters_has_personalities.PRIMARY'

因此,它试图再次插入这个东西,但我只是希望它不被更新,如果不改变。

我该知道些什么?

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2022-01-16 10:45:10

自从切换到DbContext工厂之后,您得到这个异常的原因是,您正在获取一个实体,并告诉工厂创建的一个全新的DbContext实例保存它,以及与它相关的任何实体。

您最肯定看到的问题不是您要更新的实体,而是子实体或该实体所引用的实体。在您的情况下,如果您试图持久化某个字符,并且该字符具有一组个性,那么由于该DBContext没有跟踪这些个性实例,它将把它们视为新的实体,并尝试并插入它们。

坦率地说,与独立实体一起工作是一件痛苦的事情。传递一个实体到一个新的DbContext实例并调用Update看起来很简单,但是除了最简单的场景之外,它很少那么简单。试图将数据访问层构建为一般实现只会增加复杂性。

在最基本的级别上,在处理具有关系的实体时,需要在持久化时将这些相关实体与DbContext相关联。但是,首先您需要检查所讨论的DbContext实例是否还没有跟踪匹配的实体,如果是的话就替换引用。

代码语言:javascript
复制
public async Task UpdateCharacterAsync(Character character) 
{
    await using var context = await _contextFactory.CreateDbContextAsync();
    var set = context.Set<Character>();

    foreach(var personality in character.Personalities)
    {
        context.Attach(personality);
    }
    set.Update(entity);
    await context.SaveChangesAsync();
}

最低限度可能会让你朝着正确的方向前进。因为我们需要了解个性--任何其他字符可能引用的东西--并确保DbContext正在跟踪这些特性,因此,如果不使用大量的复杂性和反射,这种方法就不可能是真正的通用方法。即使如此,这种方法也不能防止愚昧,也没有效率。如果同一个相关实体可能在一组关系中多次出现,且传递到该实体的引用不是相同的引用,则在尝试第二次附加该实体时可能会遇到错误。这在处理反序列化实体图时尤为重要,例如使用MVC或Ajax调用,其中实体和相关数据是从JSON反序列化的。例如,如果字符和个性具有对CreatedBy用户实体的引用,则需要附加这些用户实体。假设您有两个对用户ID #1的引用,其中它们是两个不同的对象,而不是对同一个对象的两个引用。当您附加第一个实例时,一切都很好,但是尝试附加第二个实例会导致一个错误,即上下文已经在跟踪具有相同ID的实例。

使用独立的实体可能会很痛苦。处理独立的实体图(关系)很容易成为一场噩梦,因为像这样的错误是情景性的。

使用Update也是没有效率的,因为这将导致UPDATE语句更新表中的所有列,不管是否发生了更改。这还会产生一些情况,如果您不使用快照和行版本控制之类的东西,则需要考虑陈旧的数据覆盖。虽然传递实体并调用Update以避免重新加载实体似乎更好,但这有几个缺点,只有在调用SaveChanges()时才会隐约暴露出来。在执行更新时,我的建议是使用一种跨行的读取和复制方法,因为这样可以在执行过程中验证数据状态,并确保DB UPDATE语句仅在值更改时运行,以及哪些列实际更改。

理想情况下,这与向Update调用发送DTO(只包含可能被更改的字段)结合在一起,在引用的情况下,只需要传递这些引用的ID或ID集合。这使有效负载大小从客户端尽可能紧凑地传递到服务器,而不是传递整个实体结构。

Automapper的Map(source, destination)方法可以帮助将更新后的值从DTO复制到加载的实体,EF的更改跟踪从那里接管,以确定UPDATE语句是否实际需要执行。

该方法最后看起来更像:

代码语言:javascript
复制
public async Task UpdateAsync(UpdateCharacterDTO characterDto) 
{
    if (characterDto == null) throw new ArgumentNullException("characterDto");

    await using var context = await _contextFactory.CreateDbContextAsync();
    var character = context.Characters
        .Include(x => x.Personalities)
        .Single(x => x.CharacterId == characterDTO.CharacterId);

    // Here you could check a row version on the DB vs. DTO to see if the DB had changed since the data used to build the DTO was read.

    _mapper.Map(characterDto, character);

    //If personalities can be added or removed...
    var existingPersonalityIds = character.Personalities.Select(x => x.PersonalityId);
    var personalityIdsToAdd = characterDto.PersonalityIds.Except(existingPersonalityIds).ToList();
    var personalityIdsToRemove = existingPersonalityIds.Except(characterDto.PersonalityIds).ToList();

    var personalitiesToAdd = await context.Personalities.Where(x => personalityIdsToAdd.Contains(x.PersonalityId).ToListAsync();
    var personalitiesToRemove = character.Personalities.Where(x => personalityIdsToRemove.Contains(x.PersonalityId)).ToList();

    foreach(var personality in personalitiesToRemove)
        character.Personalities.Remove(personality);

    foreach(var personality in personalitiesToAdd)
        character.Personalities.Add(personality);

    await context.SaveChangesAsync();
}

其中,CharacterDTO是一个数据容器,包含可以更新的ID和字段,以及个性ID的集合。(用户可能已经添加或删除项)

它会产生更多的代码,但是它应该很容易跟踪它正在做的事情,并且对于预期的EF跟踪/引用没有任何混淆。理想情况下,您可以通过使操作更具有原子性来避免这种复杂性,例如会有更简单的调用来添加和删除个性和其他相关元素,而不是试图在一个顶级update方法中更新整个对象图。像这样的Update方法工作得很好,但前提是同一个DbContext实例从开始到结束都要跟踪所涉及的实体。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/70723347

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档