一对多关系数据库保存异常:GroupId重复跟踪问题排查及优化请求
问题描述
保存一对多关系数据至数据库时,系统持续抛出GroupId被重复跟踪的异常,但仓储层查询时已启用AsNoTracking()。希望能解决该异常,同时对现有代码进行优化。
DepartmentController代码
[HttpGet] public async Task<IActionResult> Details (string id) { var department = await _repository.GetByIdAsync(id); if (department != null) { var deparment = new EditDepartmentViewModel() { DepartmentId = department.DepartmentId, DepartmentName = department.DepartmentName, PeopleInDepartment = _repository.GetNamesByDepartment(id), }; return View(deparment); } return RedirectToAction("Index"); } [HttpPost] public async Task<IActionResult> Details(EditDepartmentViewModel model) { model.DepartmentId = await _repository.GetIdByNameAsync(model.DepartmentName); model.Persons = _repository.PersonsInDep(model.DepartmentId); var userId = _contextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); var user = await _userRepository.GetByIdAsync(userId); var department = await _repository.GetByIdAsync(model.DepartmentId); if (department != null) { department.DepartmentName = model.DepartmentName; department.Persons = model.Persons; if (!string.IsNullOrWhiteSpace(model.PeopleInDepartment)) { var names = model.PeopleInDepartment.Split(new[] {", "}, StringSplitOptions.RemoveEmptyEntries); var alreadyin = _repository.GetNamesByDepartment(model.DepartmentId); var alinasList = alreadyin.Split(new[] {", "}, StringSplitOptions.RemoveEmptyEntries); if (names.Count() >= alinasList.Count()) { foreach (var name in names) { if (alreadyin.Contains(name) == false) { var person = new Person { PersonId = Guid.NewGuid().ToString(), Name = name.Trim(), DepartmentId = department.DepartmentId, GroupPerDay = new List<PersonInGroupPerDay>(), }; var dg = new DayGroup() { Id = Guid.NewGuid().ToString(), Group = await _groupRepository.GetByDepartment(department), }; var p = new PersonInGroupPerDay() { Id = Guid.NewGuid().ToString(), Person = person, DayGroup = dg, Groups = new List<Group>(), }; p.Groups.Add(dg.Group); person.GroupPerDay.Add(p); department.Persons.Add(person); } _repository.Update(department); } } else { foreach(var name in alinasList) { if (model.PeopleInDepartment.Contains(name) == false) { var person = await _repository.GetByNameAsync(name); if (person != null) { var ps = await _repository.GetByPerson(person); foreach (var p in ps) { _repository.DeletePIGPD(p); } _repository.DeletePerson(person); } } _repository.Update(department); } } } return RedirectToAction("Index"); } return RedirectToAction("Index"); }
DepartmentRepository代码
public bool Save() { var saved = _context.SaveChanges(); return saved > 0; } public bool Update(Department department) { _context.Update(department); return Save(); } public async Task<Department> GetByIdAsync(string id) { var dep = new Department(); dep.DepartmentId = id; dep.DepartmentName = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.DepartmentName).FirstOrDefaultAsync(); dep.Persons = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(p => p.Persons).FirstOrDefaultAsync(); dep.Users = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.Users).FirstOrDefaultAsync(); dep.DayDepartments = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.DayDepartments).FirstOrDefaultAsync(); dep.Group = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.Group).FirstOrDefaultAsync(); dep.Days = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.Days).FirstOrDefaultAsync(); dep.Months = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.Months).FirstOrDefaultAsync(); return dep; }
部门详情视图代码
@model QLAnCaRedo.Web.ViewModels.EditDepartmentViewModel <div class="container mb-3"> <div class="row pt-4"> <div class="col-6"> <h2 class="text-primaary"> 编辑部门 </h2> </div> </div> </div> <form method="post" action="Details"> <div class="mb-3"> <label for="" class="form-label">部门名称</label> <input type="text" class="form-control" asp-for="DepartmentName" /> </div> <div class="mb-3"> <label for="" class="form-label">部门人员</label> <input type="text" class="form-control" asp-for="@Model.PeopleInDepartment" /> </div> <button type="submit" class="btn btn-primary">提交</button> <a type="button" class="btn btn-danger" asp-controller="Department" asp-action="Delete" asp-route-id="@Model.DepartmentId"> 删除 </a>
模型定义
public class Department { [Key] public string DepartmentId { get; set; } public string DepartmentName { get; set; } public List<Person> Persons { get; set; } public Group Group { get; set; } } public class Group { [Key] public string GroupId { get; set; } [Required] public AppUser AppUser { get; set; } public Department Department { get; set; } [Required] public List<DayGroup> DayGroups { get; set; } } public class DayGroup { public string Id { get; set; } public DateTime SubcriptionDate { get; set; } public Group Group { get; set; } public List<PersonInGroupPerDay> PersonInGroups { get; set; } } public class PersonInGroupPerDay { public string Id { get; set; } public DayGroup DayGroup { get; set; } public Person Person { get; set; } [DefaultValue(0)] public int Set1 { get; set; } [DefaultValue(0)] public int Set2 { get; set; } [DefaultValue(0)] public int Set3 { get; set; } public List<Group> Groups { get; set; } } public class EditDepartmentViewModel { public string DepartmentId { get; set; } public string DepartmentName { get; set; } public string PeopleInDepartment { get; set; } public List<Person> Persons { get; set; } }
问题原因分析
- 重复跟踪异常根源:
GetByIdAsync虽用了AsNoTracking(),但_groupRepository.GetByDepartment(department)返回的Group对象可能已被上下文跟踪(该方法未启用AsNoTracking())。将已跟踪的Group赋值给新DayGroup后,调用_context.Update(department)时,上下文会尝试再次跟踪该Group,触发重复跟踪异常。 - Update调用时机错误:循环内部多次调用
_repository.Update(department),频繁触发状态变更与保存,既降低效率,也易导致实体状态混乱。
解决方案
- 避免关联对象重复跟踪:
- 修改
_groupRepository.GetByDepartment方法,添加AsNoTracking();或仅查询GroupId,手动构造仅含GroupId的Group对象,避免引入已跟踪实体:var groupId = await _groupRepository.GetGroupIdByDepartment(department); var dg = new DayGroup() { Id = Guid.NewGuid().ToString(), Group = new Group { GroupId = groupId } };
- 修改
- 调整Update调用时机:将
_repository.Update(department)移至循环外部,所有实体变更完成后仅调用一次,避免重复提交。
代码优化建议
- 重构GetByIdAsync方法:当前多次查询数据库效率极低,改为单次查询并关联加载所需数据:
public async Task<Department> GetByIdAsync(string id) { return await _context.Departments .AsNoTracking() .Include(d => d.Persons) .Include(d => d.Users) .Include(d => d.DayDepartments) .Include(d => d.Group) .Include(d => d.Days) .Include(d => d.Months) .FirstOrDefaultAsync(d => d.DepartmentId == id); } - 简化人员同步逻辑:用集合操作替代字符串分割对比,避免名称含特殊字符时出错,减少数据库查询次数:
var existingPersonNames = department.Persons.Select(p => p.Name).ToHashSet(); var submittedNames = names.Select(n => n.Trim()).ToHashSet(); // 添加新人员 foreach (var name in submittedNames.Except(existingPersonNames)) { // 创建Person及关联对象逻辑 } // 删除移除的人员 foreach (var name in existingPersonNames.Except(submittedNames)) { // 删除Person及关联对象逻辑 } _repository.Update(department); - 避免重复获取DepartmentId:视图已传递
DepartmentId,直接使用model.DepartmentId即可,无需通过名称查询,避免名称重复时出错。 - 添加事务处理:在人员增删操作中使用事务,确保操作原子性,避免数据不一致。
内容的提问来源于stack exchange,提问作者Trung Đặng Đức
相关产品推荐
相关产品推荐

