Entity Framework Core - 更新相关集合

8

我将尝试更新ProjectModel内的ProjectEmployees集合。我希望删除所有旧值并设置新值。

我的模型:

public class Project
{
    ... 
    public ICollection<ProjectEmployee> ProjectEmployees { get; set; }
}

public class ProjectEmployee
{
    public int ProjectId { get; set; }
    public virtual Project Project { get; set; }
    public int UserId { get; set; }
    public virtual Employee Employee { get; set; }
}

public class Employee
{
    public int UserId { get; set; }
    public User User { get; set; }
    ...
}
public class ProjectGroupModel //ViewModel
{
    public int ProjectId { get; set; }
    public ICollection<Employee> ProjectEmployees { get; set; }
}

这是典型的多对多关系。

我的控制器操作:

    [HttpPost("group")]
    public async Task<IActionResult> CreateGroup([FromBody] ProjectGroupModel pro)
    {
            var dbProject = await _context.Project
                .Include(p=>p.ProjectEmployees)
                .FirstAsync(p => p.ProjectId == pro.ProjectId);
            dbProject.ProjectEmployees.Clear();

            foreach (var emp in pro.ProjectEmployees)
            {
                dbProject.ProjectEmployees.Add(new ProjectEmployee()
                {
                    UserId = emp.UserId
                });
            }

            await _context.SaveChangesAsync();

            return Ok();
    }

当pro.ProjectEmployees为空时,从dbProject.ProjectEmployees中删除了所有记录,如果dbProject.ProjectEmployees为空,则添加了来自模型的新记录,但是当dbProject.ProjectEmployees不为空时,我无法设置新记录:
错误:
“无法跟踪实体类型'ProjectEmployee'的实例,因为已经跟踪了另一个具有相同键的此类型的实例。对于大多数键类型,在添加新实体时,如果未设置键(即如果键属性分配了其类型的默认值),则会创建唯一的临时键值。如果您明确为新实体设置键值,请确保它们不与现有实体或为其他新实体生成的临时值发生冲突。在附加现有实体时,请确保只有一个具有给定键值的实体实例附加到上下文中。”
我尝试以数百种方式修复此操作,但总是出现问题。

这似乎是主键冲突。当你创建新的ProjectEmployee()时,你只设置了UserId。你不应该也设置ProjectId吗? - Jakub Rusilko
@MuhammadNasir 我正在获取一个Project的记录,我应该在哪里使用ToList()? - Kuba
@JakubRusilko 我不这么认为,正如我所说,当dbProject.ProjectEmployees为空时,新记录会被正确添加。 - Kuba
@IvanStoev 我明白,我正在尝试在添加新行之前删除与此项目相关联的所有行,但我仍然遇到了这个错误。 - Kuba
@IvanStoev 我之前考虑过这个问题。我在调用Clear()后直接添加了额外的SaveChanges,但问题仍然存在 :/ - Kuba
显示剩余8条评论
3个回答

4

这并不是一种完美的解决方法,但我能够让它工作的唯一办法是在添加新项目之前从相应的 DbSet 中删除项目并调用 SaveChanges

    var dbProject = await _context.Project
        .Include(p=>p.ProjectEmployees)
        .FirstAsync(p => p.ProjectId == pro.ProjectId);

    if (dbProject.ProjectEmployees.Any())
    {
        _context.ProjectEmployee.RemoveRange(dbProject.ProjectEmployees);
        await _context.SaveChangesAsync();
    }

    foreach (var emp in pro.ProjectEmployees)
    {
        dbProject.ProjectEmployees.Add(new ProjectEmployee()
        {
            UserId = emp.UserId
        });
    }
    
    await _context.SaveChangesAsync();

1
目前我不在乎它是否完美,因为它能正常工作!非常感谢。寻找最理想的解决方案不值得。我浪费了很多时间在这上面。有时候最简单的方式是最好的 ;) - Kuba
1
那么最优解是什么?我有一种强烈的感觉,肯定有更好的解决方案。 - Mohammed Noureldin

4

另一个SO问题相关联。

我可以在这里使用您的类回答它。

然后使用我制作的扩展来删除未选择的内容并将新选择的内容添加到列表中。

    public static void TryUpdateManyToMany<T, TKey>(this DbContext db, IEnumerable<T> currentItems, IEnumerable<T> newItems, Func<T, TKey> getKey) where T : class
    {
        db.Set<T>().RemoveRange(currentItems.Except(newItems, getKey));
        db.Set<T>().AddRange(newItems.Except(currentItems, getKey));
    }

    public static IEnumerable<T> Except<T, TKey>(this IEnumerable<T> items, IEnumerable<T> other, Func<T, TKey> getKeyFunc)
    {
        return items
            .GroupJoin(other, getKeyFunc, getKeyFunc, (item, tempItems) => new { item, tempItems })
            .SelectMany(t => t.tempItems.DefaultIfEmpty(), (t, temp) => new { t, temp })
            .Where(t => ReferenceEquals(null, t.temp) || t.temp.Equals(default(T)))
            .Select(t => t.t.item);
    }

使用它看起来像这样:
 var model = db.Employees
             .Include(x => x.ProjectEmployees)
             .FirstOrDefault(x => x.EmployeeId == employee.EmployeeId);

 db.TryUpdateManyToMany(model.ProjectEmployees, listOfNewProjectIds
 .Select(x => new ProjectEmployee
 {
     ProjectId = x,
     EmployeeId = employee.EmployeeId
 }), x => x.ProjectId );

0

我认为旧对象并没有真正从数据库中删除。你只是调用了Clear(),这还不够。尝试这样做:

[HttpPost("group")]
public async Task<IActionResult> CreateGroup([FromBody] ProjectGroupModel pro)
{
    var dbProject = await _context.Project
        .Include(p=>p.ProjectEmployees)
        .FirstAsync(p => p.ProjectId == pro.ProjectId);

    foreach (var old in dbProject.ProjectEmployees)
        {
            _context.ProjectEmployee.Remove(old);
        }

    dbProject.ProjectEmployees.Clear();

    foreach (var emp in pro.ProjectEmployees)
        {
            dbProject.ProjectEmployees.Add(new ProjectEmployee()
            {
                UserId = emp.UserId
            });
        }

    await _context.SaveChangesAsync();

    return Ok();
}

在这里,我假设您的上下文(_context.ProjectEmployee)已经设置正确。如果您的上下文中没有明确设置此集合,请添加它。


我尝试过了。不幸的是,它返回相同的错误。我确定Clear()函数工作正常,因为当pro.ProjectEmployee为空时,数据库集合被清除了。 - Kuba
内存中的集合已被清除。但是,请检查记录是否真正从数据库中删除了。你能确认一下吗? - Jakub Rusilko
他们是。我认为正如IvanStoev所说,这是ef更改跟踪的问题。即使行被删除,它也不知道这一点。 - Kuba
将要删除的对象的状态更改为已删除。 - mvermef

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接