简体   繁体   English

EF6 插入重复项以连接表

[英]EF6 inserts duplicates to join table

I have following issue - I'm using generic repository to detach and reattach entity on update operation to prevent accessing it from two different objects.我有以下问题 - 我正在使用通用存储库在更新操作时分离和重新附加实体,以防止从两个不同的对象访问它。

This is how Update operation looks like:这是更新操作的样子:

        public void Update(TEntity entityToUpdate)
        {
            if (this.context.Entry(entityToUpdate).State != EntityState.Detached)
            {
            this.context.Entry(entityToUpdate).State = EntityState.Detached;
            }

            this.context.Entry(entityToUpdate).State = EntityState.Modified;
        }

When using two entities lets say like user and course使用两个实体时,可以说用户和课程

    public class User
    {
        public Guid UserId { get; set; }

        public string Email { get; set; }

        public ICollection<Course> Courses{ get; set; }
    }
}

and Course和课程

public class Course
{
    public string Name { get; set; }

    public Guid CourseId { get; set; }

    public virtual ICollection<User> Users { get; set; }
}
public class CourseDto
{
    public string Name { get; set; }

    public Guid CourseId { get; set; }

    public virtual ICollection<Guid> Users { get; set; }
}

and I try to update course's users with following code:我尝试使用以下代码更新课程的用户:

  public async Task<Course> Update(Guid existingCourseId, CourseDto courseModel)
    {

        var course = await course.Repository.Query(true)
            .Include(c=> c.Users)
            .Where(e => e.CourseId == existingCourseId )
            .FirstOrDefaultAsync();

        if (course == null)
        {
            return null;
        }

        course.Users = await FindUsersByIds(courseModel.Users);

        course.Name = courseModel.Name;

        courseRepository.Update(course);

        await this.unitOfWork.SaveChangesAsync();

        return course;
    }

it doesn't work when I want to for example update only Name property.例如,当我只想更新 Name 属性时,它不起作用。 If Users property doesn't change and there is at least one user it will try to insert record to the CourseUser join table violating primary key constraint instead of 'noticing' that it is already existing in database.如果用户属性没有改变并且至少有一个用户,它将尝试将记录插入到违反主键约束的 CourseUser 连接表中,而不是“注意到”它已经存在于数据库中。

Edit:编辑:

Additionally when I use Entry(entityToUpdate).State = EntityState.Unchanged before changing it to modified and move repository.Update() call before overwriting entity properties it works all fine.此外,当我在将其更改为修改之前使用 Entry(entityToUpdate).State = EntityState.Unchanged 并在覆盖实体属性之前移动 repository.Update() 调用时,一切正常。 If somebody could explain this behaviour to me I would be really grateful如果有人可以向我解释这种行为,我将非常感激

In a nutshell, simply don't do this with detaching and setting state to modified.简而言之,不要通过分离并将 state 设置为已修改来执行此操作。 You're already doing the right thing by transporting details to update via a DTO.通过传输详细信息以通过 DTO 进行更新,您已经在做正确的事情。 Let EF's change tracking do it's thing and just call SaveChanges() .让 EF 的更改跟踪来做这件事,只需调用SaveChanges()即可。

I'm not sure what you're concern is warranting the Detach to "prevent accessing".我不确定你关心的是保证 Detach “阻止访问”。 All this does it tell the DbContext to treat the item as untracked, but then by setting the state to Modified, you're immediately tracking it again.所有这一切都告诉 DbContext 将该项目视为未跟踪,但随后通过将 state 设置为已修改,您将立即再次跟踪它。 The Main reasons you don't want to do this is that by setting the entity state to "Modified" you are telling EF to generate an UPDATE statement for the entire record regardless of whether any value was actually modified rather than optimizing an UPDATE statement to be run for values it detected had changed, and only if they had changed.不想这样做的主要原因是,通过将实体 state 设置为“已修改”,您是在告诉 EF 为整个记录生成 UPDATE 语句,而不管是否实际修改了任何值,而不是将 UPDATE 语句优化为运行它检测到的值已更改,并且仅它们已更改时才运行。

The issue you are likely seeing is because of this line:您可能看到的问题是因为这条线:

course.Users = await FindUsersByIds(courseModel.Users);

I would bet money that your FindUsersByIds method is returning a non-tracked set of User entities either by AsNoTracking() or detaching those user entities before returning them.我敢打赌,您的 FindUsersByIds 方法将通过AsNoTracking()返回一组未跟踪的用户实体,或者在返回之前分离这些用户实体。 You have already eager loaded users for the given course you loaded.您已经为您加载的给定课程预先加载了用户。 You might think this tells EF to remove existing references and replace them with the desired ones, but it doesn't.您可能会认为这会告诉 EF 删除现有引用并将其替换为所需的引用,但事实并非如此。 It just "adds" a set of Users that EF will treat as new associations where-by some of those associations already still exist in the database.它只是“添加”了一组用户,EF 会将这些用户视为新关联,其中一些关联已经存在于数据库中。

As a general rule when working with entities.作为与实体合作时的一般规则。 Never, under any circumstances reset/overwrite a collection of related entities.在任何情况下都不要重置/覆盖相关实体的集合。 Not to clear it and not to change the values associated with an entity.不要清除它,也不要更改与实体关联的值。

If the model contains a revised list of IDs, then the proper way to update the set is to identify any users that need to be added, and any that need to be removed.如果 model 包含修改的 ID 列表,那么更新集合的正确方法是识别需要添加的任何用户,以及需要删除的任何用户。 Load references for the ones that need to be added to associate, and remove any from the eager loaded relationships.为需要添加关联的引用加载引用,并从急切加载的关系中删除任何引用。 Also, where you expect 0 or 1 result, use SingleOrDefault rather than FirstOrdefault .此外,如果您期望 0 或 1 结果,请使用SingleOrDefault而不是FirstOrdefault First methods should only be used with an OrderBy clause where you could expect more than one entry and want a reliable, repeatable First. First方法只能与OrderBy子句一起使用,在该子句中您可能期望多个条目并需要一个可靠的、可重复的 First。 The exception to this would be when working with Linq against in-memory ordered sets where you can guarantee one unique find where First() will perform faster while Single() would scan the entire set.例外情况是使用 Linq 处理内存中的有序集合时,您可以保证一个唯一的查找,其中 First() 将执行得更快,而 Single() 将扫描整个集合。 With EF generating queries First() generates a TOP(1) SQL statement where Single() generates a TOP(2) SQL statement so that performance assumption gets shot down to a bare minimum difference to enforce that expectation.使用 EF 生成查询First()生成一个 TOP(1) SQL 语句,其中Single()生成一个 TOP(2) SQL 语句,以便将性能假设降低到最小差异以强制执行该期望。

var course = await course.Repository.Query(true)
    .Include(c=> c.Users)
    .Where(e => e.CourseId == existingCourseId)
    .SingleOrDefaultAsync();

if (course == null)
    return null;

var existingUserIds = course.Users.Select(u => u.UserId);
var userIdsToAdd = courseModel.Users.Except(existingUserIds).ToList();
var userIdsToRemove = existingUserIds.Except(courseModel.Users).ToList();

if (userIdsToRemove.Any())
{
    var usersToRemove = course.Users
        .Where(u => userIdsToRemove.Contains(u.UserId))
        .ToList();
    foreach(var user in usersToRemove)
        course.Users.Remove(user);
}
if (userIdsToAdd.Any())
{
   var usersToAdd = FindUsersByIds(userIdsToAdd); // Important! Ensure this does NOT detach or use AsNoTracking()
   foreach(var user in usersToAdd)
       course.Users.Add(user);
}

course.Name = courseModel.Name;

await this.unitOfWork.SaveChangesAsync();

return course;

Basically this inspects the IDs selected to pick out the ones to add and remove, then proceeds to modify the eager loaded collection if needed.基本上,这会检查选择的 ID 以选择要添加和删除的 ID,然后在需要时继续修改预先加载的集合。 The change tracking will take care of determining what, if any SQL needs to be generated.更改跟踪将负责确定需要生成什么 SQL。

If the Users collection is exposed as List<User> then you can use AddRange and RemoveRange rather than the foreach loops.如果用户集合公开为List<User>那么您可以使用AddRangeRemoveRange而不是foreach循环。 This won't work if they are exposed as IList<User> or IHashSet<User> or ICollection<User> .如果它们暴露为IList<User>IHashSet<User>ICollection<User> ,这将不起作用。

Edit:编辑:

Based on the error mentioned, I would suggest starting by at least temporarily removing some of the variables in the equation around the particular implementation of the Repository and Unit of Work patterns and working with a scoped DbContext to begin with:根据提到的错误,我建议首先至少暂时删除方程式中围绕存储库和工作单元模式的特定实现的一些变量,并使用作用域 DbContext 开始:

For a start try this edit:首先尝试此编辑:

using (var context = new AppDbContext())
{
    var course = await context.Courses
        .Include(c=> c.Users)
        .Where(e => e.CourseId == existingCourseId)
        .SingleOrDefaultAsync();

    if (course == null)
        return null;

    var existingUserIds = course.Users.Select(u => u.UserId);
    var userIdsToAdd = 

courseModel.Users.Except(existingUserIds).ToList(); courseModel.Users.Except(existingUserIds).ToList(); var userIdsToRemove = existingUserIds.Except(courseModel.Users).ToList(); var userIdsToRemove = existingUserIds.Except(courseModel.Users).ToList();

    if (userIdsToRemove.Any())
    {
        var usersToRemove = course.Users
            .Where(u => userIdsToRemove.Contains(u.UserId))
            .ToList();
        foreach(var user in usersToRemove)
            course.Users.Remove(user);
    }
    if (userIdsToAdd.Any())
    {
       var usersToAdd = FindUsersByIds(userIdsToAdd); // Important! Ensure this does NOT detach or use AsNoTracking()
       foreach(var user in usersToAdd)
           course.Users.Add(user);
    }

    course.Name = courseModel.Name;

    await context.SaveChangesAsync();
    context.Entry(course).State = EntityState.Detached; // Necesssary evil as this method is returning an entity.
    return course;
}

This is only intended as a temporary measure to help identify if your repository or unit of work could be leading to transient DbContexts being used to load and track entities.这只是作为一种临时措施来帮助确定您的存储库或工作单元是否可能导致瞬态 DbContexts 被用于加载和跟踪实体。 This could still have issues depending on what happens to the Course after this method returns it.这可能仍然存在问题,具体取决于此方法返回后课程发生的情况。 In this case since we are leaving the scope of the DbContext instance that is tracking it, we detach it.在这种情况下,由于我们要离开正在跟踪它的 DbContext 实例的 scope,因此我们将其分离。 The next step would be to ensure that a DbContext, either directly or accessible through the unit of work can be injected and that it is guaranteed to be scoped to the web request (if web) or a scope suited to the unit of work this operation is part of.下一步是确保可以注入直接或可通过工作单元访问的 DbContext,并保证其范围为 web 请求(如果是 Web)或适合此操作的工作单元的 scope是其一部分。 (A bit more work for things like WPF desktop applications) (对于 WPF 桌面应用程序之类的东西还有更多工作要做)

Normally for something like a web application you would want to ensure that a DbContext has a lifetime scope of the web request (or shorter) but not Transient.通常对于像 web 应用程序这样的应用程序,您需要确保 DbContext 具有 web 请求(或更短)的生命周期 scope 但不是瞬态的。 We want to ensure that all operations within a unit of work reference the same DbContext instance otherwise you end up working with entities that might be tracked by multiple DbContexts, or start introducing code to mix tracked and untracked (detached) entities to get around problems when passing entity references around.我们希望确保一个工作单元中的所有操作都引用同一个 DbContext 实例,否则您最终会使用可能被多个 DbContext 跟踪的实体,或者开始引入代码以混合跟踪和未跟踪(分离)实体以解决以下问题传递实体引用。 Working with detached entities requires a lot of extra boiler-plate, disciplined coding to ensure DbContexts are working with the correct, single reference of entities to avoid "already tracked" type errors or duplicate data insertion / PK violation exceptions.使用分离的实体需要大量额外的样板、规范的编码,以确保 DbContext 使用正确的、单一的实体引用,以避免“已跟踪”类型错误或重复数据插入/PK 违规异常。 Ie pre-checking each and every DbSet.Local in a entity graph of related entities for any currently tracked instances with the same ID and replacing references when wanting to attach an entity graph to a DbContext that isn't already tracking that instance.即预先检查相关实体的实体图中的每个 DbSet.Local 是否有任何当前跟踪的具有相同 ID 的实例,并在想要将实体图附加到尚未跟踪该实例的 DbContext 时替换引用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM