Entity Framework Core中更新/删除多对多关系的正确方法是什么?

huangapple go评论65阅读模式
英文:

What is the proper way to update/delete many-to-many relationships in Entity Framework Core?

问题

使用Entity Framework创建关联表的方法,当两个实体之间存在多对多的关系时,如何进行更新和删除操作?

例如,如果我有以下实体:

public class Trip
{
    [Key]
    public Guid TripId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    [ValidateNever]
    public IEnumerable<Tag> Tags { get; set; }
}

public class Tag
{
    [Key]
    public Guid TagId { get; set; }
    public string Name { get; set; }

    [ValidateNever]
    public IEnumerable<Trip> Trips { get; set; }
}

当我创建或更新旅行时,我想要选择一个标签列表。

要将旅行实体添加到数据库中,您可以编写如下代码:

public async Task<Trip> AddTripAsync(Trip trip)
{
    foreach(var tag in trip.Tags)
    {
        _dbContext.Attach(tag);
    }
    _dbContext.Trips.Add(trip);
    await _dbContext.SaveChangesAsync();
    return trip;
}

这个方法运行良好。

但是如何更新Trip实体呢?我无法弄清楚如何从EF创建的关联表中删除记录。我尝试了如下代码:

public async Task<EntityOne> Update(Trip trip){
    // 从数据库中获取包含标签对象列表的旅行
    Trip? tripFromDb = await GetTripAsync(temp => temp.TripId == trip.TripId, "Tags");
    if (tripFromDb == null)
    {
        return trip;
    }
    tripFromDb.Name = trip.Name;
    tripFromDb.Description = trip.Description;
    foreach (var tag in trip.Tags)
    {
        var tagFromDb = tripFromDb.Tags.SingleOrDefault(x => x.TagId == tag.TagId);
        if (tagFromDb != null)
        {
            _dbContext.Attach(tagFromDb);
        }
        else
        {
            tripFromDb.Tags.Add(tag);
        }
    }
    await _dbContext.SaveChangesAsync();    
    return entityToCreate;
}

但是什么也没有改变。更新是否可以通过这种方式完成,还是我必须自己为TripsTags关联表创建记录?

编辑:
以下代码修复了问题:

public async Task<EntityOne> Update(Trip trip){
    // 从数据库中获取包含标签对象列表的旅行
    Trip? tripFromDb = _dbContext.Trips.FirstOrDefault(t => t.TripId == trip.TripId);
    if (tripFromDb == null)
    {
        return trip;
    }

    tripFromDb.Name = trip.Name;
    tripFromDb.Description = trip.Description;
    _dbContext.Entry(tripFromDb).Collection("Tags").Load();
    var existingTagIds = tripFromDb.Tags.Select(x => x.TagId).ToList();
    var updatedTagIds = trip.Tags.Select(x => x.TagId).ToList();
    var tagIdsToAdd = updatedTagIds.Except(existingTagIds);
    var tagIdsToRemove = existingTagIds.Except(updatedTagIds);

    if (tagIdsToRemove.Any())
    {
        var tagsToRemove = tripFromDb.Tags.Where(x => tagIdsToRemove.Contains(x.TagId)).ToList();
        foreach(var tag in tagsToRemove)
            tripFromDb.Tags.Remove(tag);
    }

    if (tagIdsToAdd.Any())
    {
        var tagsToAdd = await _dbContext.Tags.Where(x => tagIdsToAdd.Contains(x.TagId)).ToListAsync();
        foreach(var tag in tagsToAdd)
            tripFromDb.Tags.Add(tag);
    }

    await _dbContext.SaveChangesAsync();    
    return tripFromDb;
}
英文:

Having two entities that have a many-to-many relationship between them using the approach where Entity Framework creates the associative table how do I do the update/delete for the two entities?

For example if I have the entities:

public class Trip
{
    [Key]
    public Guid TripId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    [ValidateNever]
    public IEnumerable&lt;Tag&gt; Tags { get; set; }
}

public class Tag
{
    [Key]
    public Guid TagId { get; set; }
    public string Name { get; set; }

    [ValidateNever]
    public IEnumerable&lt;Trip&gt; Trips { get; set; }
}

When I create/update a trip I want to select a list of tags.

To add a trip entity to the db I wrote something like:

public async Task&lt;Trip&gt; AddTripAsync(Trip trip)
{
	foreach(var tag in trip.Tags)
	{
		_dbContext.Attach(tag);
	}
	_dbContext.Trips.Add(trip);
	await _dbContext.SaveChangesAsync();
	return trip;
}

It works well.

How can I update the Trip entity? I can't figure out how to remove records from the associative table created by EF
I tried something like this:

public async Task&lt;EntityOne&gt; Update(Trip trip){
	// gets the trip from db including tag objects list
	Trip? tripFromDb = await GetTripAsync(temp =&gt; temp.TripId == trip.TripId, &quot;Tags&quot;);
	if (tripFromDb == null)
	{
		return trip;
	}
	tripFromDb.Name = trip.Name;
	tripFromDb.Description = trip.Description;
	foreach (var tag in trip.Tags)
	{
		var tagFromDb = tripFromDb.Tags.SingleOrDefault(x =&gt; x.TagId == tag.TagId);
		if (tagFromDb != null)
		{
			// if the tag already exists in the trip&#39;s tags list don&#39;t try to add it again
			_dbContext.Attach(tagFromDb);
		}
		else
		{
			tripFromDb.Tags.Add(tag);
		}
	}
	
	await _dbContext.SaveChangesAsync();	
	
	return entityToCreate;
}

And nothing changes. Can the update be done this way or do I have to create the TripsTags associative table myself for this?

EDIT:
This fixed it:

public async Task&lt;EntityOne&gt; Update(Trip trip){
    // gets the trip from db including tag objects list
    //Trip? tripFromDb = await GetTripAsync(temp =&gt; temp.TripId == trip.TripId, &quot;Tags&quot;);
	Trip? tripFromDb = _dbContext.Trips.FirstOrDefault(t =&gt; t.TripId == trip.TripId);
    if (tripFromDb == null)
    {
        return trip;
    }


    tripFromDb.Name = trip.Name;
    tripFromDb.Description = trip.Description;
    _dbContext.Entry(tripFromDb).Collection(&quot;Tags&quot;).Load();
    var existingTagIds = tripFromDb.Tags.Select(x =&gt; x.TagId).ToList();
    var updatedTagIds = trip.Tags.Select(x =&gt; x.TagId).ToList();
    var tagIdsToAdd = updatedTagIds.Except(existingTagIds);
    var tagIdsToRemove = existingTagIds.Except(updatedTagIds);

    if (tagIdsToRemove.Any())
    {
       var tagsToRemove = tripFromDb.Tags.Where(x =&gt; tagIdsToRemove.Contains(x.TagId)).ToList();
       foreach(var tag in tagsToRemove)
           tripFromDb.Tags.Remove(tag);
    }

    if (tagIdsToAdd.Any())
    {
        var tagsToAdd = await _dbContext.Tags.Where(x =&gt; tagIdsToAdd.Contains(x.TagId)).ToListAsync();
        foreach(var tag in tagsToAdd)
            tripFromDb.Tags.Add(tag);
    }

    
    await _dbContext.SaveChangesAsync();    
    
    return tripFromDb;
}

答案1

得分: 0

实体代表数据状态。当它们从 DbContext 中分离时,它们只应被视为数据的某一时刻快照。如果将该分离的实体序列化并发送到视图,然后在请求中传回服务器,那么它不能且永远不应被视为实际数据状态的代表,因为它可能是陈旧的,可能不完整,或者在最坏的情况下可能被篡改。个人不建议以这种方式将实体用作 DTOs/ViewModels,因为编写用于处理实体的方法永远不应该担心它们是传递的实际跟踪的实体,还是分离的或重新构建的字段被强制转换为实体。

然而,要解决问题的关键是,EF 可以将多对多关系视为多对一的“引用”,其中连接表只需要两个外键作为复合键。A 包含了 B 的集合,每个 B 都包含了 A 的集合。很可能反序列化的假“实体”将具有一个包含“B”的“A”集合,但是这些“B”实例不会包含关于它们所属的“A”的信息。由于序列化深度到客户端和数据返回时的可用性,它们将成为它们曾经代表的实体的截断存根,这不适合以这种不完整和不可信的状态“附加”到 DbContext。为了处理更新关系,我们需要更加明确地确定需要添加或删除的关联:

public async Task<EntityOne> Update(Trip trip){
    // 从数据库中获取包含标签对象列表的旅行
    Trip? tripFromDb = await GetTripAsync(temp => temp.TripId == trip.TripId, "Tags");
    if (tripFromDb == null)
    {
        return trip;
    }

    tripFromDb.Name = trip.Name;
    tripFromDb.Description = trip.Description;

    var existingTagIds = tripFromDb.Tags.Select(x => x.TagId).ToList();
    var updatedTagIds = trip.Tags.Select(x => x.TagId).ToList();
    var tagIdsToAdd = updatedTagIds.Except(existingTagIds);
    var tagIdsToRemove = existingTagIds.Except(updatedTagIds);

    if (tagIdsToRemove.Any())
    {
       var tagsToRemove = tripFromDb.Tags.Where(x => tagIdsToRemove.Contains(x.TagId)).ToList();
       foreach(var tag in tagsToRemove)
           tripFromDb.Tags.Remove(tag);
    }

    if (tagIdsToAdd.Any())
    {
        var tagsToAdd = await _dbContext.Tags.Where(x => tagIdsToAdd.Contains(x.TagId)).ToListAsync();
        foreach(var tag in tagsToAdd)
            tripFromDb.Tags.Add(tag);
    }

    await _dbContext.SaveChangesAsync();    

    return tripFromDb;
}

这里的区别在于,我们使用请求中提供的数据来确定可能已添加或删除的引用,然后使用它来更新实际的当前数据状态。传入的分离“实体”本身不受信任,也不适合重新附加和持久化。在需要添加关联的情况下,我们通过 ID 获取这些标签并关联它们。EF 将自动处理连接记录。

英文:

Entities represent data state. When they are detached from a DbContext, then they should only be relied on as a point in time snapshot of the data. If that detached entity was serialized and sent to a view, then passed back to the server in a Request, then it cannot, and should never be trusted to be representative of actual data state as it may be stale, it may not be complete, or at worst, it may be tampered with. Personally I do not recommend ever using entities as DTOs/ViewModels in this way as methods written to work with entities should never need to worry about whether they are passed real, tracked entities versus detached or reconstituted fields cast as an entity.

To get to the crux of the problem though, a many-to-many relationship can be treated by EF much like a many-to-one "reference" where that joining table just needs the two FKs as a composite key. A has a collection of Bs, and each B has a collection of As. Chances are that the deserialized fake "entity" will have an "A" with a collection of "B"s, however those "B" instances won't contain information about the "A"s that they belong to. They will be cut down stubs of the entities they once represented due to serialization depth going to the client and data available when coming back. This isn't something suited to being "attached" to the DbContext in such an incomplete and untrustworthy state. To handle updating the relationships we need to be a bit explicit to determine what associations need to be added or removed:

public async Task&lt;EntityOne&gt; Update(Trip trip){
    // gets the trip from db including tag objects list
    Trip? tripFromDb = await GetTripAsync(temp =&gt; temp.TripId == trip.TripId, &quot;Tags&quot;);
    if (tripFromDb == null)
    {
        return trip;
    }


    tripFromDb.Name = trip.Name;
    tripFromDb.Description = trip.Description;

    var existingTagIds = tripFromDb.Tags.Select(x =&gt; x.TagId).ToList();
    var updatedTagIds = trip.Tags.Select(x =&gt; x.TagId).ToList();
    var tagIdsToAdd = updatedTagIds.Except(existingTagIds);
    var tagIdsToRemove = existingTagIds.Except(updatedTagIds);

    if (tagIdsToRemove.Any())
    {
       var tagsToRemove = tripFromDb.Tags.Where(x =&gt; tagIdsToRemove.Contains(x.TagId)).ToList();
       foreach(var tag in tagsToRemove)
           tripFromDb.Tags.Remove(tag);
    }

    if (tagIdsToAdd.Any())
    {
        var tagsToAdd = await _dbContext.Tags.Where(x =&gt; tagIdsToAdd.Contains(x.TagId)).ToListAsync();
        foreach(var tag in tagsToAdd)
            tripFromDb.Tags.Add(tag);
    }

    
    await _dbContext.SaveChangesAsync();    
    
    return tripFromDb;
}

The difference here is that we use the provided data coming in the request to determine which references might have been added or removed, then use that to update the real current data state. The passed in detached "entity" itself isn't trusted, or suited to be re-attached and persisted. Where there are associations to add, we fetch those Tags by ID and associate them. EF will take care of the joining records automatically.

huangapple
  • 本文由 发表于 2023年5月30日 03:49:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/76359992.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定