Каскадное удаление EF 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

}

Соответствующая таблица была специально объявлена ​​в Context

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);
    }

В настоящее время вышеуказанное решение «работает», но:

а) занимает более 2 минут б) Кажется, передняя часть React выдает ошибку 502 (но см. Выше). Конечно, FE претендует на 502.

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
0
228
2

Ответы 2

Ok. Немного сложно понять, почему это медленно. Насколько велика структура данных и т. д.

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

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

Привет Кристиан и спасибо за ваш ответ. Мне придется изменить используемый фрагмент кода, так как я уже заметил и исправил обнаруженную вами проблему повторно вложенным SaveChangesAsync(). На самом деле с кодом, который вы только что увидели, я получил «ожидалось, что изменю одну строку, но изменил нулевые исключения строк».

technorabble 15.03.2019 11:13

Что касается самого объекта, то он крошечный. Если бы я выполнял эту операцию в электронной таблице Excel или в SSMS, я бы ожидал, что она займет меньше секунды. На самом деле, в других частях приложения я избегаю узких мест типа «получи это, теперь возьми это, теперь то», просто всасывая всю таблицу. Исследование здесь показывает, что когда мы SaveChangesAsync(), EF последовательно загружает каждую строку и выполняет удаление. Кажется, это также имеет место, если используется RemoveRange, хотя в какой-то момент прошлой ночью я, кажется, припоминаю, что читал о версии RemoveRange с одним ударом ...?

technorabble 15.03.2019 11:36

@technorabble Я думаю, что большая проблема здесь в том, что вы выполняете рекурсивное удаление неопределенной глубины. Для EF очень сложно эффективно преобразовать его в эффективный SQL. Если это небольшой объект - даже с каскадным удалением - я ожидаю, что он будет быстрым. Единственным реальным решением было бы выполнение пользовательских операторов SQL непосредственно в вашем контексте.

Kristian Barrett 15.03.2019 11:50

Вот почему мне было интересно, будет ли более подходящим docs.microsoft.com/en-us/ef/ef6/modeling/code-first/fluent/…. Сообщите БД, чего ожидать, тогда код бизнес-логики может просто удалить один указанный PD, а база данных обработает все остальное.

technorabble 15.03.2019 12:31

@technorabble это может быть, хотя я все еще думаю, что LINQ будет трудно эффективно переводить. Но лучшее, что вы можете сделать, это попытаться создать оператор, запускающий его против вашей БД, и посмотреть план выполнения оператора LINQ, чтобы увидеть, является ли SQL более эффективным.

Kristian Barrett 15.03.2019 14:02

К сожалению, это самореферентное отношение, и каскадное удаление нельзя использовать из-за проблемы «множественных каскадных путей» - ограничения базы данных 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 с предыдущего уровня в качестве фильтра (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. Вы должны проверить, какой из них больше подходит для вашего случая.

Другие вопросы по теме