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 🙂