You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

一对多关系数据库保存异常: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; }
}

问题原因分析

  1. 重复跟踪异常根源:GetByIdAsync虽用了AsNoTracking(),但_groupRepository.GetByDepartment(department)返回的Group对象可能已被上下文跟踪(该方法未启用AsNoTracking())。将已跟踪的Group赋值给新DayGroup后,调用_context.Update(department)时,上下文会尝试再次跟踪该Group,触发重复跟踪异常。
  2. Update调用时机错误:循环内部多次调用_repository.Update(department),频繁触发状态变更与保存,既降低效率,也易导致实体状态混乱。

解决方案

  1. 避免关联对象重复跟踪:
    • 修改_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 }
      };
      
  2. 调整Update调用时机:将_repository.Update(department)移至循环外部,所有实体变更完成后仅调用一次,避免重复提交。

代码优化建议

  1. 重构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);
    }
    
  2. 简化人员同步逻辑:用集合操作替代字符串分割对比,避免名称含特殊字符时出错,减少数据库查询次数:
    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);
    
  3. 避免重复获取DepartmentId:视图已传递DepartmentId,直接使用model.DepartmentId即可,无需通过名称查询,避免名称重复时出错。
  4. 添加事务处理:在人员增删操作中使用事务,确保操作原子性,避免数据不一致。

内容的提问来源于stack exchange,提问作者Trung Đặng Đức

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 08:43:09