Как удалить объект, ссылающийся на себя?

#c# #entity-framework #entity-framework-core

Вопрос:

Контекст

В моем текущем проекте существует иерархическая структура папок, сохраненная в базе данных. Сущность заключается в следующем:

 public class Folder
{
    public string Id { get; set; }
    public string ParentFolderId { get; set; }
    public Folder ParentFolder { get; set; }
    public List<Folder> ChildFolders { get; set; } // Relationship property
    // Other properties
}
 

Только корневая папка имеет значение null как ParentFolderId . Все остальные папки имеют значение, отличное от нуля ParentFolderId . EFCore не позволяет настраивать каскадное удаление ParentFolderId .

Вопрос

Когда папка удаляется, я хотел бы также рекурсивно удалить все вложенные папки. Как это может быть реализовано?

Что я пытался

В DbContext:

 protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    builder.Entity<Folder>()
        .HasOne(e => e.ParentFolder)
        .WithMany(e => e.ChildFolders)
        .OnDelete(DeleteBehavior.ClientCascade);
}
 

Удалить вызов:

 public void DeleteFolder(string folderId)
{
    var folder = await _db.Folders
        .Include(d => d.ParentFolder)
        .Include(f => f.ChildFolders)
        .FirstOrDefaultAsync(d => d.Id == folderId);

    if (folder == null) return;
    if (folder.ParentFolderId == null) return null;

    _db.Folders.Remove(folder);
    _db.SaveChanges();
}
 

Однако в этой ситуации я загружаю вложенные папки только на один уровень ниже в иерархии. Я могу звонить ThenInclude кучу раз, но не могу гарантировать, что достиг всех вложенных папок.

Комментарии:

1. Вы можете рекурсивно вызывать DeleteFolder вложенные папки перед удалением основной папки. Или рекурсивно получить все вложенные папки, а затем сделать одну RemoveRange .

2. @juharr Ты, наверное, прав. Как мне это сделать, зная только один идентификатор папки?

3. У вас уже есть ChildFolders , поэтому вы просто зацикливаетесь на этом и передаете идентификаторы DeleteFolder или другому методу, который соберет все идентификаторы вложенных папок.

Ответ №1:

Вот способ сделать это, включив метод для получения всех вложенных папок. Недостатком является то, что для этого требуется один выбор для каждого уровня вложенных папок в иерархии. Альтернативой может быть SQL, где вы можете написать рекурсивный запрос, чтобы удалить все идентификаторы, а затем удалить их.

 public void DeleteFolder(string folderId)
{
    var folder = await _db.Folders
        .FirstOrDefaultAsync(d => d.Id == folderId);

    if (folder == null) return;

    var foldersToDelete = await SubFolders(folder.Id);
    foldersToDelete.Add(folder);

    _db.Folders.RemoveRange(foldersToDelete);
    _db.SaveChanges();
}

public async Task<List<Folder>> SubFolders(string folderId)
{
    var subFolders = await _db.Folders
        .Where(d => d.ParentFolderId == folderId)
        .ToListAsync();

    var allFolders = new List<Folder>();
    foreach(var subFolder in subFolders)
    {
        allFolders.Add(subFolder);
        allFolders.AddRange(await SubFolders(subFolder.Id));
    }

    return allFolders;
}
 

Обратите внимание на это с помощью run forever, если у вас есть какие-либо циклы в ваших папках (например, родительская папка A-папка B, а родительская папка B-папка A).

Комментарии:

1. Отличный способ подойти к этой проблеме. Спасибо. В моем коде я удаляю каждую дочернюю папку одну за другой в конце рекурсивной функции.

2. @M. Azyoksul Есть варианты этого, которые будут работать. Некоторые из них могут быть более эффективными, чем это, а другие-нет, поэтому я бы посоветовал вам протестировать их, чтобы увидеть, что работает лучше всего.

Ответ №2:

В итоге я немного изменил ответ @juharr. Вместо того, чтобы создавать кучу списков, я создаю один список и передаю его ссылку среди вызовов рекурсивных функций.

Вот мой окончательный код:

 public void DeleteFolder(string userId, string folderId)
{
    var folder = await _db.Folders
        .Include(d => d.ParentFolder)
        .Include(f => f.ChildFolders)
        .FirstOrDefaultAsync(d => d.Id == folderId);

    if (folder == null) return;
    if (folder.ParentFolderId == null) return;

    var foldersToBeDeleted = new List<Folder>();

    folder.ChildFolders.ForEach(childFolder => PopulateSubFolder(childFolder.Id, foldersToBeDeleted));
    foldersToBeDeleted.Add(folder);

    _db.Folders.RemoveRange(foldersToBeDeleted);
    _db.SaveChanges();
}

private void PopulateSubFolder(string folderId, ICollection<Folder> foldersToDelete)
{
    var folder = _db.Folders
        .Include(f => f.ChildFolders)
        .FirstOrDefault(f => f.Id == folderId);

    if (folder == null) return;

    // Add child folders
    folder.ChildFolders.ForEach(subFolder => PopulateSubFolder(subFolder.Id, foldersToDelete));

    // Add current folder
    foldersToDelete.Add(folder);
}