Entity Framework Core 8.0.8中父类与[Owned]属性类的数据库更新问题
问题分析与解决方案
问题背景
使用EF Core的[Owned]特性将RefreshToken关联到User实体,保存更新时抛出DbUpdateConcurrencyException,提示"数据库操作预期影响1行,但实际影响0行"。
相关代码参考
实体定义
public class User { public int Id { get; set; } public string Username { get; set; } public RefreshToken RefreshToken { get; set; } } [Owned] public class RefreshToken { public string Token { get; set; } public DateTime Expires { get; set; } public bool IsRevoked { get; set; } }
业务与仓储代码
// 业务保存逻辑 public async Task SaveRefreshToken(User user, RefreshToken newToken) { removeOldRefreshTokens(user); user.RefreshToken = newToken; await _userRepository.Update(user); await _unitOfWork.SaveChangesAsync(); } // 移除旧令牌方法 private void removeOldRefreshTokens(User user) { if (user.RefreshToken != null && user.RefreshToken.Expires < DateTime.UtcNow) { user.RefreshToken.IsRevoked = true; } } // 通用仓储Update方法 public async Task Update(T entity) { _dbContext.Entry(entity).State = EntityState.Modified; }
核心原因
- Owned实体追踪缺失:查询
User时未加载RefreshToken,EF Core无法追踪Owned实体的变更,导致更新语句无有效字段。 - Update方法误用:直接设置
EntityState.Modified会强制更新主实体所有字段,但Owned实体的变更可能未被正确识别。 - 并发令牌不匹配:若
User配置了并发令牌(如[Timestamp]),实体内存值与数据库值不一致会触发并发异常。
解决方案
1. 确保查询时加载Owned实体
获取User必须包含RefreshToken,让EF Core能追踪其变更:
var user = await _userRepository.GetByIdAsync(userId, include: u => u.Include(u => u.RefreshToken));
2. 修正通用仓储Update逻辑
避免直接设置EntityState.Modified,改用值映射确保Owned实体变更被识别:
public async Task Update(T entity) { var existingEntity = await _dbContext.Set<T>().FindAsync(GetEntityKey(entity)); if (existingEntity != null) { _dbContext.Entry(existingEntity).CurrentValues.SetValues(entity); // 手动同步Owned实体属性 if (entity is User user) { _dbContext.Entry(existingEntity.RefreshToken).CurrentValues.SetValues(user.RefreshToken); } } } // 辅助方法:获取实体主键值 private object GetEntityKey(T entity) { return _dbContext.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties .Select(p => p.PropertyInfo.GetValue(entity)).FirstOrDefault(); }
3. 利用EF Core自动追踪简化代码
若User是上下文追踪的实体,无需手动调用Update,直接修改后保存即可:
public async Task SaveRefreshToken(int userId, RefreshToken newToken) { var trackedUser = await _userRepository.GetByIdAsync(userId, include: u => u.Include(u => u.RefreshToken)); removeOldRefreshTokens(trackedUser); trackedUser.RefreshToken = newToken; await _unitOfWork.SaveChangesAsync(); }
4. 并发冲突处理(可选)
若确为并发问题,可捕获异常后重试:
public async Task SaveRefreshToken(int userId, RefreshToken newToken) { int retryCount = 3; while (retryCount > 0) { try { var trackedUser = await _userRepository.GetByIdAsync(userId, include: u => u.Include(u => u.RefreshToken)); removeOldRefreshTokens(trackedUser); trackedUser.RefreshToken = newToken; await _unitOfWork.SaveChangesAsync(); return; } catch (DbUpdateConcurrencyException) { retryCount--; if (retryCount == 0) throw; } } }
内容的提问来源于stack exchange,提问作者Ashish
相关产品推荐
相关产品推荐

