EF Core Cascading удаляет скорость? - PullRequest
3 голосов
/ 15 марта 2019

Я работаю над установленной (но изменчивой, предполагая, что существующие данные выживает при любых изменениях) кодовой базой и исследую некоторые очень медленные удаления.До сих пор я только усугублял ситуацию, поэтому мы здесь.Я отклонил большинство своих попыток внесенных изменений ниже, чтобы избежать дополнительной ненужной путаницы.

Существует класс данных 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; }

Наряду с отношением Fluent API, определенным дляContext.OnModelCreating

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 минут. б) Кажется, что для интерфейса React ошибка 502 (но см. выше).Конечно, FE требует 502

Мой основной вопрос: есть ли способ повысить скорость удаления, например, путем определения каскадного удаления в FluentAPI (моя попытка столкнулась с проблемой при попытке применить миграцию)?Но я приветствую любые обсуждения того, что может заставить FE сообщить о плохих воротах.

Ответы [ 2 ]

2 голосов
/ 17 марта 2019

К сожалению, это самоотражающееся отношение, и каскадное удаление не может быть использовано из-за проблемы «нескольких каскадных путей» - ограничения базы данных SqlServer (и, возможно, другой) (у Oracle такой проблемы нет).

Лучший способ обработки в базах данных, которые не поддерживают «несколько каскадных путей», - это использовать триггер базы данных («вместо удаления»).

Но, допустим, мы хотим обработать его с помощью клиентского кода в EF Core. Вопрос в том, как эффективно загрузить рекурсивную древовидную структуру (еще одна непростая задача в EF Core из-за отсутствия поддержки рекурсивных запросов).

Проблема с вашим кодом состоит в том, что он использует глубина вначале алгоритм, который выполняет много запросов к базе данных. Более подходящий и эффективный способ - использовать алгоритм дыхание вначале - простыми словами, загружая предметы на уровень . Таким образом, количество запросов к базе данных будет максимальной глубиной в дереве, которая будет намного меньше, чем количество элементов.

Один из способов реализации - применить запрос с начальным фильтром, а затем использовать 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 s с предыдущего уровня в качестве фильтра (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 голос
/ 15 марта 2019

Хорошо. Трудно понять, почему это так медленно. Насколько велика структура данных и т. Д.

Первое, что бросается в глаза, когда я смотрю на приведенный выше код, это следующее:

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().

...