Soft delete w EfCore

Soft delete w EfCore

Czyli jak oznaczyć rekord jako usunięty?

Wstęp

W prawdziwym świecie raczej nie usuwa się rekordów z bazy na stałe. Często są one tylko oznaczane jako „rekordy usunięte”. Później te rekordy można archiwizować lub faktycznie usunąć po jakimś czasie. Dlaczego tak się robi?

Czasem są to powody biznesowe, a czasem nawet prawne. Wyobraź sobie chociażby sytuację, w której tworzysz forum internetowe. Ktoś napisał bardzo obraźliwy komentarz lub nawet komuś groził, po czym komentarz został przez autora usunięty. Jednak sprawa została zgłoszona na policję. W tym momencie masz możliwość sprawdzenia czy i jaki komentarz został utworzony, a nawet kiedy został „oznaczony do usunięcia”.

To oczywiście prosty przykład, ale w świecie bardzo ważnych danych, które można usunąć jednym kliknięciem, takie usuwanie na stałe nie jest raczej pożądane. W końcu to użytkownik jest najgorszą częścią systemu, a my niejako musimy chronić ten system przed użytkownikami.

Mechanizm oznaczania rekordów jako usuniętych nazywa się Soft Delete (miękkie usuwanie).

Implementacja

Flaga w modelu

Implementacja soft delete w EfCore jest raczej prosta. Przede wszystkim musisz posiadać w swoim modelu bazodanowym jakieś pole, które będzie trzymało informację o tym, czy rekord jest usunięty, czy nie. Ja używam zawsze bazowej klasy do wszystkich modeli bazodanowych, która wygląda mniej więcej tak:

public abstract class DbItem
{
    public Guid Id { get; set; }
    public bool IsDeleted { get; set; }
}

Tutaj rolę tej flagi pełni oczywiście właściwość IsDeleted.

Pobieranie danych nieusuniętych

Następny krok to konfiguracja EfCore w taki sposób, żeby pobierać dane tylko rekordów nieusuniętych. Tak jakbyś do każdego zapytania dodał warunek WHERE isDeleted = 0.

Oczywiście można to zrobić automatycznie. Podczas konfiguracji modelu w metodzie OnModelCreating dodaj filtr, używając HasQueryFilter, np:

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

    builder.Entity<MyDbModel>()
        .HasQueryFilter(x => x.IsDeleted == false);
}

Ten QueryFilter musisz dodać do każdego modelu.

Ja do konfiguracji modeli używam interfejsu IEntityTypeConfiguration i bazowej klasy, więc jest to łatwiejsze. Opiszę to w innym artykule.

W każdym razie HasQueryFilter na modelu robi tyle, że do każdego zapytania dla tego modelu dodaje właśnie taki warunek WHERE.

Jeśli jednak będziesz chciał w jakimś konkretnym przypadku pobrać dane również oznaczone do usunięcia, możesz zignorować QueryFilters w zapytaniu Linq:

var data = dbContext.MyModels
    .Where(x => x.Name == "blabla")
    .IgnoreQueryFilters(); //to zignoruje wszystkie QueryFilters skonfigurowane na modelu

Modyfikacja danych zamiast usuwania

W ostatnim kroku trzeba troszkę zmienić działanie EfCore. Jeśli ktoś chce usunąć rekord, napisze:

dbContext.MyModels.Remove(data);
dbContext.SaveChanges();

Domyślnie EfCore utworzy zapytanie „DELETE”. Trzeba to zmienić w taki sposób, żeby usuwanie rekordu tak naprawdę modyfikowało flagę IsDeleted. Dlatego dodajemy zdarzenia do DbContext. Kod się sam opisuje:

public AppDbContext(DbContextOptions options)
    :base(options)
{
    SavingChanges += AppDbContext_SavingChanges; //dodajemy handlery do zdarzeń
    SavedChanges += AppDbContext_SavedChanges;
    SaveChangesFailed += AppDbContext_SaveChangesFailed;
}

private void AppDbContext_SaveChangesFailed(object sender, SaveChangesFailedEventArgs e)
{
    ChangeTracker.AutoDetectChangesEnabled = true;
}

private void AppDbContext_SavedChanges(object sender, SavedChangesEventArgs e)
{
    ChangeTracker.AutoDetectChangesEnabled = true;
}

//to się dzieje podczas dbContext.SaveChanges()
private void AppDbContext_SavingChanges(object sender, SavingChangesEventArgs e)
{
    ChangeTracker.DetectChanges(); //wykrywamy zmiany w modelu

    foreach(var entry in ChangeTracker.Entries()) //przechodzimy przez wszystkie zmienione modele
    {
        switch(entry.State)
        {
            case EntityState.Deleted: //jeśli model ma zostać usunięty
                entry.CurrentValues[nameof(DbItem.IsDeleted)] = true; //zmień pole IsDeleted na true
                entry.State = EntityState.Modified; //i oznacz go jako zmodyfikowany, a nie usunięty
                break;
        }
    }

    ChangeTracker.AutoDetectChangesEnabled = false;
}

Dzięki takiej operacji zmodyfikujesz rekord zamiast go usuwać.

Być może w pewnych sytuacjach będziesz chciał usunąć rekord za pomocą zapytania SQL. Tutaj już musisz sam pamiętać o tym, żeby zamiast DELETE zrobić UPDATE odpowiedniego pola.


To tyle. Jeśli czegoś nie zrozumiałeś, potrzebujesz dodatkowych wyjaśnień albo znalazłeś błąd w artykule, koniecznie podziel się w komentarzu 🙂

Obrazek dla artykułu dzięki rawpixel.com – pl.freepik.com

Podziel się artykułem na: