Я работаю над устоявшейся (но изменчивой, если предположить, что существующие данные выживают при любых изменениях) кодовой базой и исследую некоторые очень медленные удаления. До сих пор мне удавалось только ухудшить ситуацию, так что вот мы здесь. Я отказался от большинства моих попыток внести изменения ниже, чтобы избежать дополнительной ненужной путаницы.
Существует класс данных 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.





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().
Что касается самого объекта, то он крошечный. Если бы я выполнял эту операцию в электронной таблице Excel или в SSMS, я бы ожидал, что она займет меньше секунды. На самом деле, в других частях приложения я избегаю узких мест типа «получи это, теперь возьми это, теперь то», просто всасывая всю таблицу. Исследование здесь показывает, что когда мы SaveChangesAsync(), EF последовательно загружает каждую строку и выполняет удаление. Кажется, это также имеет место, если используется RemoveRange, хотя в какой-то момент прошлой ночью я, кажется, припоминаю, что читал о версии RemoveRange с одним ударом ...?
@technorabble Я думаю, что большая проблема здесь в том, что вы выполняете рекурсивное удаление неопределенной глубины. Для EF очень сложно эффективно преобразовать его в эффективный SQL. Если это небольшой объект - даже с каскадным удалением - я ожидаю, что он будет быстрым. Единственным реальным решением было бы выполнение пользовательских операторов SQL непосредственно в вашем контексте.
Вот почему мне было интересно, будет ли более подходящим docs.microsoft.com/en-us/ef/ef6/modeling/code-first/fluent/…. Сообщите БД, чего ожидать, тогда код бизнес-логики может просто удалить один указанный PD, а база данных обработает все остальное.
@technorabble это может быть, хотя я все еще думаю, что LINQ будет трудно эффективно переводить. Но лучшее, что вы можете сделать, это попытаться создать оператор, запускающий его против вашей БД, и посмотреть план выполнения оператора LINQ, чтобы увидеть, является ли SQL более эффективным.
К сожалению, это самореферентное отношение, и каскадное удаление нельзя использовать из-за проблемы «множественных каскадных путей» - ограничения базы данных 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. Вы должны проверить, какой из них больше подходит для вашего случая.
Привет Кристиан и спасибо за ваш ответ. Мне придется изменить используемый фрагмент кода, так как я уже заметил и исправил обнаруженную вами проблему повторно вложенным SaveChangesAsync(). На самом деле с кодом, который вы только что увидели, я получил «ожидалось, что изменю одну строку, но изменил нулевые исключения строк».