EF Core:使用分页从大型子集合中删除断开连接的 EF Core:使用分页从大型子集合中删除

EF Core: Disconnected delete from a large child collection with pagination

提问人:Pavel 提问时间:11/15/2023 更新时间:11/15/2023 访问量:29

问:

我的问题基于一个类似的问题,但增加了分页的复杂性。

public class Parent
{
    public Parent()
    {
        this.Children = new List<Child>();
    }

    public int Id { get; set; }

    public virtual ICollection<Child> Children { get; set; }
}

public class Child
{
    public int Id { get; set; }

    public int ParentId { get; set; }

    public string Data { get; set; }
}

假设 Children 集合有 1000 条记录:

{ 1, 2, 3, ...998, 999, 1000 }

public Parent Get(int parentId, int childrenPageSize)
{
    var existingParent = _dbContext.Parents
    .Where(p => p.Id == parentId)
    .Include(p => p.Children.Take(childrenPageSize))
    .AsNoTracking()
    .SingleOrDefault();
}

通过分页,我们只取前 10 个孩子:

{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }

然后,用户在前端应用程序中编辑 Parent 对象,并删除集合中的 2 个子对象,然后单击“保存”以更新父项:

{ 3, 4, 5, 6, 7, 8, 9, 10 }

我在存储库类中使用 Update 方法

public void Update(UpdateParentModel model)
{
    var existingParent = _dbContext.Parents
    .Where(p => p.Id == model.Id)
    .Include(p => p.Children)
    .SingleOrDefault();

    // Delete children
    foreach (var existingChild in existingParent.Children.ToList())
    {
        if (!model.Children.Any(c => c.Id == existingChild.Id))
            _dbContext.Children.Remove(existingChild);
    }
}

:当 Entity Framework 加载 existingParent 时,它会急切加载 1000 个子项的整个集合。model 参数包含 8 个子项的集合,因为删除了两个子项。如何告诉 Entity Framework 仅删除已删除的 2 个子项?

注意:我想避免使用 ExecuteDelete、RawSQL 或存储过程。我需要能够调用 _dbContext.SaveChangesAsync()。

asp.net 实体框架 asp.net-core asp.net-web-api 实体框架核心

评论


答:

1赞 Moho 11/15/2023 #1

UpdateParentModel应包含一个属性,该属性表示要删除的对象的 ID。然后,您可以附加已删除的新对象,并且仅为该可枚举属性中的每个 ID 值设置标识值,从而避免了从数据库加载任何内容的需要。IEnumerable<int>ChildChild

用于操作的模型不需要与它们所操作的一个或多个实体匹配。以最有用/最有效的方式打包数据。

foreach( var childId in model.RemovedChildIDs )
{
    var entry = dbContext.Attach( new Child
    {
        Id = childId,
    } );

    entry.State = EntityState.Deleted;
}

…

dbContext.SaveChangesAsync();

评论

0赞 Pavel 11/15/2023
谢谢。我假设它是更新和插入的相同解决方案?只是在这些对象的前端保留一个单独的集合,并将它们与父模型分开发送到后端?
0赞 Moho 11/15/2023
如果要插入新的父项,则所有子项都将是新对象,因此要跟踪的状态不会发生变化(至少根据您向我们展示的内容),但是如果要在具有相同请求的父项中添加和删除子项,则可以使用包含新子实体的单独集合
0赞 Steve Py 11/15/2023 #2

如果您在这两种情况下都有完整的集,那么它可以确定要删除哪些集,但您有不同的集,一个是分页集,一个是完整的集。给定 8 个项目,EF 无法知道最初的 10 个项目是什么,除非你告诉它。一种选择是给它提供页面大小和页码,假设您可以更改子视图的“页面”:

public void Update(UpdateParentModel model, int pageNumber = 1, int pageSize = 10)
{
    var existingParent = _dbContext.Parents
    .Where(p => p.Id == model.Id)
    .Include(p => p.Children
        .OrderBy(c => c.Id)
        .Skip((pageNumber-1)*pageSize)
        .Take(PageSize)
        .ToList())
    .SingleOrDefault();


    var existingChildrenIds = existingParent.Children.Select(x => x.Id);
    var updatedChildrenIds = model.Children.Select(x => x.Id);
    var childrenIdsToRemove = existingChildrenIds.Except(updatedChildrenIds);

    if (childrenIdsToRemove.Any())
    {
        childrenToRemove = existingParent.Children
            .Where(x => childrenIdsToRemove.Any(x.Id))
            .ToList();
        foreach(var child in childrenToRemove)
            existingParent.Children.Remove(child);
    }     
}

这也避免了迭代超过 1000 个(在本例中为 10 个)子项以查看是否需要删除任何子项。超过 10 个项目它不会有太大区别,但在更大的套装上它可以。

这里需要注意的是,当使用“跳过”和“获取”时,你应该始终包含一个子句,以确保分页的结果是可预测的和可重复的。如果没有它,更大的数据集可能会根据数据库索引分页以不同或不可预测的顺序返回。OrderBy

评论

0赞 Pavel 11/16/2023
我们也考虑过这个解决方案,但问题是我们的前端 angular 应用程序需要跟踪分页,以及删除了子项的页面。例如,在第 1 页的前 10 项中,用户删除了一个子项。然后在分页器中单击“下一步”,该分页器现在显示第 11-20 项,他在那里删除了一个子项。然后单击“最后”,这会将用户带到最后一组子项 990-1000,并删除其中的项目。因此,发送到服务器的集合将包含 { 1 - 20, 990 - 1000 },缺少 3 个项目,并且难以发送页码信息。
0赞 Steve Py 11/16/2023
是的,那会变得丑陋。即使您累积了删除并按页面#分组调用,这些行被选中删除,也必须以相反的页面顺序执行(即从第100页删除所有内容,然后是第2页,然后是第1页)或选择实体中的分页条目也会受到影响。我会倾向于莫哈的建议。不过,为了安全起见,在尝试附加子项之前检查本地缓存,以防上下文碰巧正在跟踪一个子项。假设您使用的是服务器端分页,则需要跟踪要删除的 Is。context.Children.Local.FirstOrDefault(x => x.Id == childId)