我正在研究已建立的(但可以更改,假设现有数据可以承受任何更改)代码库,并研究一些非常缓慢的删除操作。到目前为止,我只是成功地使事情变得更糟,所以我们来了。为了避免增加不必要的混乱,我在下面取消了大多数尝试的更改。
有一个数据类ProductDefinition,该类模拟与例如文件夹结构:每个PD(根除外)将有一个父级,但是就像一个文件夹可以有多个子级一样。
public class ProductDefinition
{
public int ID { get; set; }
// each tree of PDs should have a 'head' which will have no parent
// but most will have a ParentPDID and corresponding ParentPD
public virtual ProductDefinition ParentProductDefinition { get; set; }
public int? ParentProductDefinitionId { get; set; }
public virtual List<ProductDefinition> ProductDefinitions { get; set; }
= new List<ProductDefinition>();
[Required]
[StringLength(100)]
public string Name { get; set; }
// etc. Fields. Nothing so large you'd expect speed issues
}
相应的表已在上下文中明确声明
public DbSet<ProductDefinition> ProductDefinitions { get; set; }
具有在Context.OnModelCreating上定义的Fluent API关系
modelBuilder.Entity<ProductDefinition>()
.HasMany(productDefinition => productDefinition.ProductDefinitions)
.WithOne(childPd => childPd.ParentProductDefinition)
.HasForeignKey(childPd => childPd.ParentProductDefinitionId)
.HasPrincipalKey(productDefinition => productDefinition.ID);
似乎已经尝试在ProductDefinitionManager类中确定删除
public static async Task ForceDelete(int ID, ProductContext context)
{
// wrap the recursion in a save so that it only happens once
await ForceDeleteNoSave(ID, context);
await context.SaveChangesAsync();
}
还有
private static async Task ForceDeleteNoSave(int ID, ProductContext context)
{
var pd = await context.ProductDefinitions
.AsNoTracking()
.Include(x => x.ProductDefinitions)
.SingleAsync(x => x.ID == ID);
if (pd.ProductDefinitions != null && pd.ProductDefinitions.Count != 0)
{
var childIDs = pd.ProductDefinitions.Select(x => x.ID).ToList();
// delete the children recursively
foreach (var child in childIDs)
{
// EDITED HERE TO CORRECTLY REFLECT THE CURRENT CODE BASE
await ForceDeleteNoSave(child, context);
}
}
// delete the PD
// mark Supplier as edited
var supplier = await context.Suppliers.FindAsync(pd.SupplierID);
supplier.Edited = true;
// reload with tracking
pd = await context.ProductDefinitions.FirstOrDefaultAsync(x => x.ID == ID);
context.ProductDefinitions.Remove(pd);
}
目前,上述解决方案“有效”,但是:
a)需要2分钟以上才能完成 b)似乎给React前端一个502错误(但见上文)。当然,FE要求赔偿502
我的主要问题是:有没有办法提高删除速度,例如通过在FluentAPI中定义级联删除(我的尝试在尝试应用迁移时遇到问题)?但我欢迎任何讨论,可能导致FE举报Bad Gateway。
答案 0 :(得分:2)
不幸的是,这是自引用关系,由于“多个级联路径”问题-SqlServer(可能还有其他)数据库的限制(Oracle没有这样的问题),无法使用级联删除。
在不支持“多个级联路径”的数据库中处理的最佳方法是使用数据库触发器(“而不是删除”)。
但是可以说我们想通过EF Core中的客户端代码来处理它。问题是如何有效地加载类似递归树的结构(由于缺乏递归查询支持,EF Core中的另一个不容易的任务)。
您的代码的问题在于它使用了 depth first 算法,该算法执行很多数据库查询。更合适和更有效的方法是使用 breath first 算法-简单来说,就是按 level 加载项目。这样,数据库查询的数量将成为树中的最大深度,该数量小于元素的数量。
一种实现方法是从具有初始过滤器的查询开始,然后使用SelectMany
获取下一个级别(每个SelectMany
都将联接添加到上一个查询)。当查询不返回数据时,该过程结束:
public static async Task ForceDelete(int ID, ProductContext context)
{
var items = new List<ProductDefinition>();
// Collect the items by level
var query = context.ProductDefinitions.Where(e => e.ID == ID);
while (true)
{
var nextLevel = await query
.Include(e => e.Supplier)
.ToListAsync();
if (nextLevel.Count == 0) break;
items.AddRange(nextLevel);
query = query.SelectMany(e => e.ProductDefinitions);
}
foreach (var item in items)
item.Supplier.Edited = true;
context.RemoveRange(items);
await context.SaveChangesAsync();
}
请注意,执行的查询急于加载相关的Supplier
,因此可以轻松地对其进行更新。
一旦收集了项目,就可以简单地将其标记为通过RemoveRange
方法删除。顺序无关紧要,因为EF Core仍然会按照依赖关系顺序应用命令。
另一种收集项目的方法是使用上一级的ID
作为过滤器(SQL IN
):
// Collect the items by level
Expression<Func<ProductDefinition, bool>> filter = e => e.ID == ID;
while (true)
{
var nextLevel = await context.ProductDefinitions
.Include(e => e.Supplier)
.Where(filter)
.ToListAsync();
if (nextLevel.Count == 0) break;
items.AddRange(nextLevel);
var parentIds = nextLevel.Select(e => e.ID);
filter = e => parentIds.Contains(e.ParentProductDefinitionId.Value);
}
我更喜欢前者。缺点是EF Core会生成巨大的表名别名,并且在深度较大的情况下也可能会遇到一些SQL连接数限制。后者没有深度限制,但可能与大IN
子句有关。您应检查哪种更适合您的情况。
答案 1 :(得分:1)
好的。确切地理解为什么这很慢有些困难。数据结构等的大小。
当我看上面的代码时,首先想到的是以下内容:
public static async Task ForceDelete(int ID, ProductContext context)
{
// wrap the recursion in a save so that it only happens once
await ForceDeleteNoSave(ID, context);
await context.SaveChangesAsync();
}
该方法被递归调用,但是每次处理完一堆孩子时,它都会调用context.SaveChagesAsync()
。这意味着当您运行代码时,您将获得多次保存和对数据库的多次调用。
这似乎是一种反模式,因为如果您的程序在中途崩溃,则已经删除了一些子代。
取而代之的是InitForceDelete()
,最后将调用context.SaveChangesAsync()
,因此全部通过一次操作完成。
类似这样的东西:
public static async Task InitForceDelete(int ID, ProductContext context)
{
// wrap the recursion in a save so that it only happens once
await ForceDeleteNoSave(ID, context);
await context.SaveChangesAsync();
}
private static async Task ForceDeleteNoSave(int ID, ProductContext context)
{
var pd = await context.ProductDefinitions
.AsNoTracking()
.Include(x => x.ProductDefinitions)
.SingleAsync(x => x.ID == ID);
if (pd.ProductDefinitions != null && pd.ProductDefinitions.Count != 0)
{
var childIDs = pd.ProductDefinitions.Select(x => x.ID).ToList();
// delete the children recursively
foreach (var child in childIDs)
{
await ForceDeleteNoSave(child, context);
}
}
var supplier = await context.Suppliers.FindAsync(pd.SupplierID);
supplier.Edited = true;
// reload with tracking
pd = await context.ProductDefinitions.FirstOrDefaultAsync(x => x.ID == ID);
context.ProductDefinitions.Remove(pd);
}
现在,第二,您应该尝试检查SQL服务器上正在执行的sql。您应该能够找到由LINQ语句触发的执行计划,并查看SQL是否完全疯狂。也许您的代码每个ProductDefinition
执行一次调用,这会使它变得非常慢。
很抱歉,我不能更加精确,但是从您提供的代码中很难给出直接的指针,除非您不断调用context.SaveChagesAsync()
。