提问人:Pavel 提问时间:11/15/2023 更新时间:11/15/2023 访问量:29
EF Core:使用分页从大型子集合中删除断开连接的 EF Core:使用分页从大型子集合中删除
EF Core: Disconnected delete from a large child collection with pagination
问:
我的问题基于一个类似的问题,但增加了分页的复杂性。
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()。
答:
UpdateParentModel
应包含一个属性,该属性表示要删除的对象的 ID。然后,您可以附加已删除的新对象,并且仅为该可枚举属性中的每个 ID 值设置标识值,从而避免了从数据库加载任何内容的需要。IEnumerable<int>
Child
Child
用于操作的模型不需要与它们所操作的一个或多个实体匹配。以最有用/最有效的方式打包数据。
foreach( var childId in model.RemovedChildIDs )
{
var entry = dbContext.Attach( new Child
{
Id = childId,
} );
entry.State = EntityState.Deleted;
}
…
dbContext.SaveChangesAsync();
评论
如果您在这两种情况下都有完整的集,那么它可以确定要删除哪些集,但您有不同的集,一个是分页集,一个是完整的集。给定 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
评论
context.Children.Local.FirstOrDefault(x => x.Id == childId)
评论