简体   繁体   中英

Entity Framework update the intermediate table of a many-to-many relation

I've got a many-to-many relation between user and project . Like:

class User 
{
    public ICollection<Project> Projects { get; set; }
}

class Project 
{
    public ICollection<User> Users { get; set; }
}

Entity Framework automatically generated the intermediate table.

The thing is I want to update the user, along with the entire list of projects. This list could've been modified in any way, projects could've been added and deleted. at the same time before the user object gets updated.

I always get the same error, that Entity Framework tried to add a duplicated entry in the intermediate table.

I've tried numerous things without success (a few listed below).

var tmp = Context.Entry(user); // user being the updated object.
tmp.State = EntityState.Modified;
tmp.Collection(e => e.Projects).IsModified = true;
        
Context.Users.Update(user);
Context.SaveChanges();

or

var tmp = Context.Users.SingleOrDefault(u => u.Id == user.Id);

if (tmp == null) 
     return null;

Context.Entry(tmp).CurrentValues.SetValues(user);
Context.SaveChanges();

return user;

or just plain old update:

Context.Users.Update(user);
Context.SaveChanges();

But none of these worked.

The issue sounds like you have a detached User Entity with a set of Projects and you want to pass that into a method, associate with the DbContext to persist the changes.

You are encountering issues with doubling up records because while you attach the user to the DbContext, it will treat each of the Project entities associated with the user as new instances because they don't reference tracked instances themselves.

Updating detached entities with associations is fairly involved, especially where you expect to possibly add or remove associations in an operation.

The recommended approach would be to load the current User and Projects from the DB then leverage Automapper to guard what values you can copy over from the detached entity, and then go through the associations to add/remove any project references that have changed. If it is possible to create a brand new project to associate to the user as part of this operation, you need to handle that as well.

var existingUser = Context.Users.Include(x => x.Projects).Single(x => x.UserId == user.UserId);
Mapper.Map(user, existingUser); 
// Where Automapper is configured with a User to User mapping with allowed 
// values to copy over, ignoring anything that cannot legally be changed.

var newProjectIds = user.Projects.Select(x => x.ProjectId).ToList();
var existingProjectIds = existingUser.Projects.Select(x => x.ProjectId).ToList();
var projectIdsToAdd = newProjectIds.Except(existingProjectIds).ToList();
var projectIdsToRemove = existingProjectIds.Except(newProjectIds).ToList();

var projectsToAdd = Context.Projects.Where(x => projectIdsToAdd.Contains(x.ProjectId)).ToList();
var projectsToRemove = existingUser.Projects.Where(x => projectIdsToRemove.Contains(x.ProjectId)).ToList();

foreach(var project in projectsToRemove)
    existigUser.Projects.Remove(project);
foreach(var project in projectsToAdd)
    existingUser.Projects.Add(project);

Context.SaveChanges();

... This example does not cover the possibility of brand new projects. If the updated user can include a brand new project then you need to detect those when looking for projectsToAdd to add any Projects from the passed in project list where the ID is in new project IDs but not found in the DB. Those detached references can be added to the User loaded from the DbContext, however you do need to handle any navigation properties that each Project might have to avoid duplication, substituting each of those with references to tracked entities, including any bi-directional reference back to the User if present.

In general, dealing with detached entities has various considerations that you need to keep in mind and handle very deliberately. It is generally much better to avoid passing detached entities around and instead aim to pass a minimal representation of the data you want to associate, then load and adjust that server-side. Usually the argument to using detached entities is to avoid having to load the data again, however this leads to more code when trying to synchronize these detached instances, and neglects the fact that data state could have changed since the detached instances were taken. The above code for instance should also be looking at entity versioning between the detached entity and the loaded state to detect if anyone else might have made changes since the detached copies were read at the start of the user process for making the changes.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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