切换到 DbContextFactory 后更新不起作用
Update not working since switch to DbContextFactory
由于 second operation was started on this context ...
,我最近改用了 ContextFactory
所以我注册了 DbContextFactory:
builder.Services.AddDbContextFactory<CharacterSheetDbContext>(
options => {
options.UseMySql(
builder.Configuration.GetConnectionString("DefaultConnection"),
new MySqlServerVersion(new Version(8, 0, 27))
);
options.EnableSensitiveDataLogging();
}
);
然后我有我的服务层:
在 ctor 中注入工厂:
public ARepository(IDbContextFactory<CharacterSheetDbContext> contextFactory) {
_contextFactory = contextFactory;
}
和更新方法(我创建上下文的地方):
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 时它会抛出以下错误:
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'
所以它试图再次插入这些东西,但我只是希望它在没有更改的情况下不更新。
我应该知道什么?提前致谢!
您在切换到 DbContext 工厂后得到该异常的原因是因为您正在获取一个实体并告诉工厂创建的全新 DbContext 实例持久保存它,以及与之关联的任何实体。
您看到的问题肯定不是您尝试更新的实体,而是该实体具有的子实体或引用实体。在您的情况下,如果您试图保留一个角色,并且该角色有一个个性集合,那么由于 DBContext 没有跟踪这些个性实例,它将把它们视为新实体并尝试插入它们。
坦率地说,与分离的实体一起工作是一种痛苦。将实体传递给新的 DbContext 实例并调用 Update
可能看起来很简单,但除了最简单的场景外,它很少这么简单。尝试将数据访问层构建为通用实现只会增加复杂性。
在最基本的层面上,在处理具有关系的实体时,您需要在持久化时将这些相关实体关联到 DbContext。但是,首先您需要检查有问题的 DbContext 实例是否尚未跟踪匹配的实体,如果是则替换引用。
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();
}
至少,这可能会让您朝着正确的方向前进。因为我们需要了解 Personalities 以及 Character 可能引用的任何其他内容,并确保 DbContext 正在跟踪这些内容,所以如果不求助于大量的复杂性和反射,这种类型的方法就不能真正成为 Generic。即使那样,这种方法也不是万无一失的,也不是有效的。如果同一个相关实体有可能在一组关系中出现多次,并且传入的对该实体的引用不是同一个引用,则在第二次尝试附加该实体时可能会遇到错误。这在处理反序列化实体图时尤其重要,例如使用 MVC 或 Ajax 调用,其中实体和相关数据是从 JSON 反序列化的。例如,如果 Character 和 Personality 引用了 CreatedBy User 实体,则需要附加这些实体。假设您有 2 个对用户 ID #1 的引用,它们是 2 个不同的对象,而不是对同一对象的 2 个引用。当您附加第一个实例时,一切都很好,但尝试附加第二个实例会导致错误,即上下文已经在跟踪具有相同 ID 的实例。
使用分离的实体可能会很痛苦。使用分离的实体图(关系)很容易成为一场噩梦,因为像这样的错误是情境性的。
使用 Update
也不是很有效,因为这将导致 UPDATE 语句更新 table 中的所有列,无论是否发生更改。如果您不使用快照和行版本控制之类的东西,这也会产生需要考虑覆盖陈旧数据的情况。虽然传递实体并仅调用 Update
以避免重新加载实体似乎更好,但这有几个缺点,只有在调用 SaveChanges()
时才会有些模糊地显示出来。在执行更新时,我的建议是使用跨读取和复制方法,因为这允许您在进行时验证数据状态,并确保 DB UPDATE 语句仅 运行 如果值发生变化,以及为了什么列实际上发生了变化。
理想情况下,这与将 DTO 发送到仅包含可能更改的字段的更新调用相结合,并且在引用的情况下,只需要为这些引用传递 ID 或 ID 集合。这使得从客户端到服务器的负载大小尽可能紧凑,而不是传递整个实体结构。
Automapper 的 Map(source, destination)
方法可以帮助将更新后的值从 DTO 复制到加载的实体,而 EF 的更改跟踪从那里接管决定是否确实需要执行 UPDATE 语句。
该方法最终看起来更像:
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 tracking/referencing。理想情况下,您可以通过使操作更加原子化来避免很多这种复杂性,例如对添加和删除个性以及其他相关元素的调用会更简单,而不是尝试在一个顶级更新方法中更新整个对象图。像那样的更新方法工作得很好,但前提是所涉及的实体从头到尾都由同一个 DbContext 实例跟踪。
由于 second operation was started on this context ...
所以我注册了 DbContextFactory:
builder.Services.AddDbContextFactory<CharacterSheetDbContext>(
options => {
options.UseMySql(
builder.Configuration.GetConnectionString("DefaultConnection"),
new MySqlServerVersion(new Version(8, 0, 27))
);
options.EnableSensitiveDataLogging();
}
);
然后我有我的服务层:
在 ctor 中注入工厂:
public ARepository(IDbContextFactory<CharacterSheetDbContext> contextFactory) {
_contextFactory = contextFactory;
}
和更新方法(我创建上下文的地方):
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 时它会抛出以下错误:
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'
所以它试图再次插入这些东西,但我只是希望它在没有更改的情况下不更新。
我应该知道什么?提前致谢!
您在切换到 DbContext 工厂后得到该异常的原因是因为您正在获取一个实体并告诉工厂创建的全新 DbContext 实例持久保存它,以及与之关联的任何实体。
您看到的问题肯定不是您尝试更新的实体,而是该实体具有的子实体或引用实体。在您的情况下,如果您试图保留一个角色,并且该角色有一个个性集合,那么由于 DBContext 没有跟踪这些个性实例,它将把它们视为新实体并尝试插入它们。
坦率地说,与分离的实体一起工作是一种痛苦。将实体传递给新的 DbContext 实例并调用 Update
可能看起来很简单,但除了最简单的场景外,它很少这么简单。尝试将数据访问层构建为通用实现只会增加复杂性。
在最基本的层面上,在处理具有关系的实体时,您需要在持久化时将这些相关实体关联到 DbContext。但是,首先您需要检查有问题的 DbContext 实例是否尚未跟踪匹配的实体,如果是则替换引用。
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();
}
至少,这可能会让您朝着正确的方向前进。因为我们需要了解 Personalities 以及 Character 可能引用的任何其他内容,并确保 DbContext 正在跟踪这些内容,所以如果不求助于大量的复杂性和反射,这种类型的方法就不能真正成为 Generic。即使那样,这种方法也不是万无一失的,也不是有效的。如果同一个相关实体有可能在一组关系中出现多次,并且传入的对该实体的引用不是同一个引用,则在第二次尝试附加该实体时可能会遇到错误。这在处理反序列化实体图时尤其重要,例如使用 MVC 或 Ajax 调用,其中实体和相关数据是从 JSON 反序列化的。例如,如果 Character 和 Personality 引用了 CreatedBy User 实体,则需要附加这些实体。假设您有 2 个对用户 ID #1 的引用,它们是 2 个不同的对象,而不是对同一对象的 2 个引用。当您附加第一个实例时,一切都很好,但尝试附加第二个实例会导致错误,即上下文已经在跟踪具有相同 ID 的实例。
使用分离的实体可能会很痛苦。使用分离的实体图(关系)很容易成为一场噩梦,因为像这样的错误是情境性的。
使用 Update
也不是很有效,因为这将导致 UPDATE 语句更新 table 中的所有列,无论是否发生更改。如果您不使用快照和行版本控制之类的东西,这也会产生需要考虑覆盖陈旧数据的情况。虽然传递实体并仅调用 Update
以避免重新加载实体似乎更好,但这有几个缺点,只有在调用 SaveChanges()
时才会有些模糊地显示出来。在执行更新时,我的建议是使用跨读取和复制方法,因为这允许您在进行时验证数据状态,并确保 DB UPDATE 语句仅 运行 如果值发生变化,以及为了什么列实际上发生了变化。
理想情况下,这与将 DTO 发送到仅包含可能更改的字段的更新调用相结合,并且在引用的情况下,只需要为这些引用传递 ID 或 ID 集合。这使得从客户端到服务器的负载大小尽可能紧凑,而不是传递整个实体结构。
Automapper 的 Map(source, destination)
方法可以帮助将更新后的值从 DTO 复制到加载的实体,而 EF 的更改跟踪从那里接管决定是否确实需要执行 UPDATE 语句。
该方法最终看起来更像:
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 tracking/referencing。理想情况下,您可以通过使操作更加原子化来避免很多这种复杂性,例如对添加和删除个性以及其他相关元素的调用会更简单,而不是尝试在一个顶级更新方法中更新整个对象图。像那样的更新方法工作得很好,但前提是所涉及的实体从头到尾都由同一个 DbContext 实例跟踪。