EF核心级联删除速度了吗?

时间:2019-03-15 09:55:33

标签: c# ef-code-first entity-framework-core

我正在研究已建立的(但可以更改,假设现有数据可以承受任何更改)代码库,并研究一些非常缓慢的删除操作。到目前为止,我只是成功地使事情变得更糟,所以我们来了。为了避免增加不必要的混乱,我在下面取消了大多数尝试的更改。

有一个数据类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。

2 个答案:

答案 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()