Pewien czas temu natknąłem się na arcyciekawy wpis na blogu Michała Gruca. Sam wpis traktuje o tym, które metody z klasy Object należy nadpisywać i jak to robić w przypadku obiektów reprezentujących encje biznesowe.
Jeszcze ciekawiej zapowiadała się dyskusja w komentarzach. Chcąc nawiązać do rozpoczętego wątku zaczętego przez Marcina Stachniuka czy używanie ID w metodach equals i hashCode ma sens i powoduje, że z CRUD dostajemy RUD.

Po co nam sztuczne ID?

Na początek zastanówmy się jaka jest rola identyfikatora w encji. Oczywiste jest, że ma on jednoznacznie opisywać dany obiekt w zbiorze obiektów tej samej klas. Dodatkowo, i jest to cecha zapożyczona z RDBMS, powinien być unikalny w skali systemu.
Wiele obiektów biznesowych można opisać za pomocą naturalnych identyfikatorów czy to prostych – adresu email czy złożonych identyfikator klienta + Timestamp. Po co zatem sztuczny identyfikator?
Jest to pozostałość z początków rozwoju aplikacji internetowych. W przypadku systemów powstających w tamtym czasie kluczową kwestią była wydajność sieci. Zazwyczaj serwer aplikacji i serwer bazy danych znajdowały się na dwóch różnych maszynach. Powodowało to, że każde ograniczenie ruchu, tu przede wszystkim ograniczenie ilości zapytań, było najważniejszym elementem pozwalającym na uzyskanie lepszych „osiągów”. Dlaczego tak się działo? Przyjrzyjmy się prostej sytuacji. Chcemy by użyszkodnik był jednoznacznie identyfikowany po emailu. Obecnie nie robi nam większej różnicy czy przed dodaniem rekordu do bazy sprawdzimy czy taki identyfikator już nie istnieje. Kiedyś robiło to znaczną różnicę. Kolejna rzecz to ograniczenie ilości kodu. Znacznie łatwiej jest za pomocą DDLa wymusić na bazie generowanie kolejnych identyfikatorów z jakiejś sekwencji niż zapewniać unikalność po stronie aplikacji. Mniej kodu to teoretycznie mniej błędów.
Osobną klasą problemów są te związane z sytuacją kiedy klucz główny w bazie danych musi być kluczem złożonym i w dodatku jest często używany jako klucz obcy albo, o zgrozo, musi być częściowo modyfikowany. Jest to sytuacja szczególna i przyjrzymy się jej dokładniej. To jednak za chwilę.

Dlaczego sztuczny identyfikator to zło?

SOLID, KISS, DRY. Mówią wam coś te pojęcia? Jeżeli tak to musicie wiedzieć, że sztuczny identyfikator łamie wszystkie te zasady.

Złamanie DRY

W każdej klasie powtarzacie coś takiego:

Listing 1. Złamanie DRY

@Entity
public class MyBussinesObject{

   @Id
   @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "my_seq")
   private long id;

   public long getId(){return id;}
   public void setId(long id){this.id=id;}

   //...
}

I takie coś do usranej śmierci. Sto tabel w bazie, sto encji sto powtórzeń. Częściowym rozwiązaniem problemu jest stworzenie wspólnej klasy AbstractEntity, czy jak w Play Framework Model, która będzie matkować wszystkim naszym zagubionym encjom… tyle tylko, że tu …

Złamanie KISS

Każde wymuszone dziedziczenie to komplikacja. Szczególnie w Javie, która nie pozwala na wielodziedziczenie. Trochę lepiej wygląda to w Scali, gdzie można użyć traitsów, ale nadal wymuszamy jakieś działania.

Złamanie SOLID

Ze względu na to, że SOLID to tak naprawdę kilka reguł to należy w tym przypadku wziąć pod uwagę iż:

  • S – Obiekt nadal posiada jedną odpowiedzialność
  • O – Jest jednak trudny w rozszerzaniu
  • L – Wymiana implementacji czy elementów architektury może być problematyczna
  • I – Nadmiarowość.
  • D – Zbytnie przywiązanie do konkretnego rozwiązania.

Identyfikatory naturalne

Naturalne identyfikatory obiektów są całkowitą odwrotnością identyfikatorów sztucznych. Z jednej strony pozwalają nam na pozbycie się nadmiarowych elementów z kodu, ale z drugiej strony wymagają znacznie więcej uwagi.
Wróćmy na chwilę do opisanej sytuacji, gdzie dodajemy użyszkodnika do bazy. Patrząc na tą sytuację raz jeszcze można łatwo dojść do wniosku, że sztuczny identyfikator jest nam zbędny ponieważ wymagamy adresu email, a ten ze swojej natury jest unikalny. Można zatem pozbyć się nadmiarowego identyfikatora zastępując go identyfikatorem naturalnym, który i tak jest sprawdzany pod kątem unikalności w systemie.

Chwila o metodach z Object

Prosty identyfikator naturalny jest zazwyczaj typem, który można stosunkowo łatwo użyć w czasie implementacji equals(), hashCode()oraz compareTo() (oj zapominamy o tej metodzie zapominamy, a to źle). Jeżeli nawet nasz obiekt nie posiada jeszcze identyfikatora, jest on null, to stosunkowo łatwo można odszukać kod, który próbuje korzystać z takiego, niespójnego i niestabilnego, obiektu. Poleci zwykle NPE i po problemie.

Problem identyfikatora złożonego

Czasami jest tak, że klucz w tabeli jest złożonym, a tym samym identyfikator też jest złożony. w takim przypadku stajemy przed kilkoma problemami.
Pierwszym problemem jest trudność określenia jak ma wyglądać taki identyfikator. Mniejsza kiedy proces tworzenia kodu następuje od bazy do klasy. Wtedy automatycznie zostaną wygenerowane odpowiednie obiekty. Gorzej gdy model danych powstaje od początku od strony obiektowej. Wtedy pokusa użycia prostego sztucznego identyfikatora jest ogromna.
Drugim z nich jest zapewnienie warunku nie-NULL dla wszystkich elementów składających się na identyfikator. Można to zrobić, w javie, wykorzystując chociażby JSR 303 – Bean Validation. Szczególnie upierdliwe są przypadki gdzie nie znamy, w momencie tworzenia identyfikatora, wszystkich danych obiektu. Z tym wiąże się bezpośrednio trzeci problem, czyli zmienność identyfikatora.
Paradoksalnie znacznie częściej niż problem z określeniem co ma być elementem identyfikatora jest zmienność tych elementów. Zdarzają się sytuacje, w których z jednej strony musimy zapewnić spełnienie warunku nie-NULL, a z drugiej nie znamy jeszcze wszystkich informacji pozwalających na opisanie obiektu.
Ciężko jest tu wymyślić jakiś rozsądny przykład, ale… wyobraźmy sobie dwa komunikujące się ze sobą systemy. Do identyfikacji komunikatów używają one stosunkowo krótkiej, cyklicznej sekwencji (automaty na pocztach). Naszym zadaniem jest przygotowanie tabeli archiwizacyjnej, w której czas przechowywania komunikatów jest znacznie dłuższy niż czas obrotu sekwencji. Ergo sekwencja, która zapewnia unikalność w komunikacji traci tą cechę w tym przypadku. Ponad to komunikaty musimy zapisywać jeszcze przed ich zarejestrowaniem przez system, a czas rejestracji będzie nową dana w naszej tabeli. Naturalnym identyfikatorem będzie zatem tandem numeru sekwencyjnego i daty rejestracji (sekwencja jest unikalna w ramach jednej daty).
Reasumując mamy taki moment w systemie gdzie znamy numer sekwencyjny, ale nie znamy daty. Komunikat czeka w bazie na aktualizację daty rejestracji albo usunięcie w przypadku błędu czy odrzucenia. Co w takiej sytuacji?
Nie można wstawić wartości NULL. Można jednak wstawić datę, która z punktu widzenia procesu biznesowego jest pozbawiona sensu. Na przykład 0 (w przypadku typu timestamp uzyskamy 1 stycznia 1970 o czym też warto pamietać). Mamy tu do czynienia ze specyficzną odmianą wzorca Null Object.

Na zakończenie

Do popełnienia tego wpisu, poza wspomnianym wyżej wpisem, skłoniła mnie ostania przygoda z tabelą, w której wykorzystujemy jako klucz główny w sumie sześć kolumn. W dodatku część z nich zmienia się w trakcie przetwarzania. Najważniejszą obserwacją jakiej dokonałem w trakcie naszych bojów jest ta, że jeżeli wykorzystujemy gdzieś w systemie relacyjny model danych to należy rozpocząć projektowanie właśnie od tego modelu, a nie od strony obiektowej. Pozwoli to na uniknięcie wielu frustrujących sytuacji, a w końcu na oszczędzenie sobie nerwów i czasu.