Prosty soft delete z wykorzystaniem NHibernate
Soft delete jest jednym z podstawowych pojęć, które spotyka się w świecie crudowych aplikacji. Właściwie trudno wyobrazić sobie, aby jakakolwiek rzeczywista biznesowa aplikacja mogła istnieć bez możliwości oznaczania rekordów jako usunięte zamiast ich fizycznego kasowania z bazy. (Niektórzy twierdzą nawet, że niczego nie powinno się z bazy usuwać - tak, na wszelki wypadek.) Oczywiście tak podstawowej funkcji nie mogło zabraknąć w moim frameworku. W tym poście opisuję w jaki sposób zaimplementowałem mechanizm soft delete w Pizzy, co przy okazji pozwala na pokazanie elastyczności i rozszerzalności NHibernate, czyli tych cech tego ORMa, których nie uświadczymy w konkrencyjnych produktach koncernu na M.
Krótka uwaga o idei soft delete
Tak, wiem o tym, że niektórzy uważają soft delete za zło i twierdzą że powinno się to realizować przez pełne audytowanie rekordów przy każdej zmianie. Może i tak, mi osobiście mechanizm oparty na dodatkowej kolumnie jeszcze nigdy nie sprawiał problemów, zaś audyt odkładający kopie całych rekordów (wszystko jedno, czy w tej samej tabeli, innej tabeli czy nawet dodatkowej bazie) wpływa dość mocno na szybkość działania i rozmiar bazy. Nie chcę iść tą drogą w prostym frameworku do nieskomplikowanych aplikacji. Ale może kiedyś… kto wie.
Oznaczanie obiektów podlegających soft delete
Pierwsza sprawa - zapewne część danych będziemy chcieli kasować trwale, a część jedynie ukrywać za pomocą soft delete. Z tego powodu potrzebna jest możliwość oznaczania wybranych klas jako ukrywane. Jako, że i tak potrzebujemy właściwości opisującej, czy obiekt jest ukryty czy też nie, tu wybór jest prosty - interfejs. O taki:
Implementacja ukrywania
Druga sprawa - jak “kasować” takie obiekty? Możliwości jest kilka:
- Zdefiniowanie interfejsu rozszerzającego
ISession
o metodę np.SoftDelete
, i oczywiście napisanie klasy go implementującej. - Dodanie takiej metody w głównym serwisie aplikacji.
- Podpięcie sie pod event rzucany przez NHibernate przed usunięciem obiektu.
Pierwsza możliwość wymusi używanie na korzystającym z frameworka programiście jakiegoś dziwnego interfejsu zamiast dobrze znanego wszystkim ISession
, a co gorsza wprowadzi dodatkowe dziedziczenie. W przypadku korzystania z soft delete, trzeba będzie pamiętać o korzystaniu z innej niż zwykła sesji NH. Dziedziczenie to jak wszyscy już chyba wiemy zło, którego trzeba unikać. Pomysł zatem odpada z punktu widzenia dobrej archiektury i czystego kodu.
Druga możliwość wygląda teoretycznie lepiej, nie wymaga dziedziczenia, ale wymaga pamiętania o tym, że jest. Po zmianie już istniejącej klasy na ukrywalną, trzeba będzie przejrzeć kod i upewnić się, że nigdzie na tej klasie nie jest wołana metoda Delete
- w przeciwnym razie dane będą usuwane mimo, że tego nie chcemy. Jeśli zrobimy to nieuważnie, to doprowadzimy do pewnej niespójności albo co gorsza znikania danych, które jednak chcemy na wszelki wypadek zostawić. To podejście jest niebezpieczne i niewygodne, więc również odpada.
Trzecia możliwość - może zostać uznana za “magiczną”, ale:
- Skoro oznaczamy obiekt jako
ISoftDeletable
, to znaczy, że chcemy aby był ukrywany, a nie usuwany. - Nie trzeba będzie o niczym pamiętać w przypadku zmiany obiektu z usuwalnego na ukrywalny, i z powrotem.
- Nie ma dodatkowych klas, o których trzeba pamiętać - mniej do nauczenia w ramach frameworka.
- Zadziała w każdym przypadku, nie tylko jeśli użytkownik będzie korzystał z serwisów crudowych dostarczanych przez Pizzę, ale także jeśli w swoim własnym serwisie będzie operował na
ISession
. Zawsze, jeśli zawołaISession.Delete
na obiekcie oznaczonym jakoISoftDeletable
nie zostanie on usunięty lecz ukryty.
Jak to zrobić w NHibernate
Ponieważ NHibernate ma bardzo rozbudowany system zdarzeń, w celu zamiany usuwania obiektu na jego ukrywanie, wystarczy właściwie zaimplementować odpowiedni event listener. W tym przypadku jest to: IPreDeleteEventListener
.
Wielkiej filozofii tu nie ma. Pobieramy obiekt, który został wysłany do metody ISession.Delete
. Jeśli ten obiekt implementuje ISoftDeletable
, zostanie mu ustawiona flaga IsDeleted
na true
i na bieżącej sesji zostanie wywołąne Update
oraz Flush
. Na końcu ifa znajduje się return true
informujący NHibernate, że sami obsłużyliśmy operację usuwania, więc ma nie robić tego sam.
Odczytywanie tylko nieusuniętych danych
Dobra, potrafimy już oznaczać rekordy jako usunięte. Teraz musimy rozwiązać kolejne zadanie - ponieważ istnieją one fizycznie w bazie, to po wykonaniu jakieś zapytanie, także takie rekordy zostaną usunięte. Nie jest to oczywiście pożądane zachowanie, trzeba je zatem jakoś odfiltrować. Można oczywiście rozbudowywać ISession
albo bazowy serwis o nowe metody, no ale wiemy już, że nie chcemy tego robić. To, czego potrzebujemy w celu filtrowania w NHibernate dla zmyłki nazywa się filtrem i jest on po prostu klasą dziedzicząca z FilterDefinition
. Wygląda ona tak:
Podobne rozwiązanie dla Entity Frameworka nazywa się znacznie bardziej intuicyjnie: “conditional mapping”, dzięki temu nikt o tym nie słyszał i nikt nie używa. Chociaż to w sumie lepiej, bo skoro nikt nie używa, to nikt nie znajdzie błędów. A więcej błędów to coś, czego w EF zdecydowanie nie trzeba. Win-win w czystej postaci.
Konfiguracja
Na koniec trzeba jakoś połączyć wszystkie te elementy i sprawić, żeby działały, czyli trzeba je dodać do konfiguracji NHibernate. W Pizzy jest jedno źródło konfiguracji, używane zarówno przez kontener IoC na potrzeby frameworka jak i aplikację inicjującą bazę danych oraz testy integracyjne. Jest nim klasa NHConfigurationFactory
. Tam, w metodzie BuildConfiguration
(oprócz wielu innych rzeczy) odbywa się rejestracja filtru oraz listenera. Wygląda to tak:
I to wszystko. Soft delete działa, a kod wygląda ślicznie, wszystko jest oparte o delegację zadań do malutkich klas spełniających SRP. Nie ma żadnych dziwnych hierarchii rozbudowanych godobjectów, które wymusiłoby używanie np. Entity Frameworka.