O identyfikacji encji i lenistwie deweloperów…

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.

12 myśli na temat “O identyfikacji encji i lenistwie deweloperów…

  1. Jest jeden bardzo ważny powód, dla którego sztuczne identyfikatory liczbowe są konieczne. Identyfikatory naturalne są zbyt duże i ich wydajność w bazie danych (w porównaniu do kilkubajtowej liczby) jest żałosna. Jak ilość wierszy idzie w miliony, a objętość tabeli w gigabajty, zapytania które łączą po kilka tabel przestają być śmieszne. Nie wspominając o kluczach złożonych…

    Poza tym faktycznie jest to bardzo wygodne. Jeśli każda encja ma liczbowy ID, można poczynić pewne założenia i uprościć sobie kod. Pod pewnymi założeniami (zbyt silnymi na equals(), ale czasem możliwymi na wyższym poziomie) można napisać generyczne porównywanie, bardzo lekki transfer, serializację ze stabilną kolejnością elementów w zbiorach itd. Wreszcie: zawsze wiesz co jest identyfikatorem bez czytania kodu i łatwo zapewnić jego niemodyfikowalność (w życiu email _może_ się zmienić).

    Regułki regułkami, ale w życiu czasem jest inaczej. Tak samo jest z normalizacją tabel – pewnie w każdej normalnej bazie celowo unika się 3NF i powiela się kolumny, nie oddziela się wszystkich zależności jeden-do-wielu albo stosuje się kolumny złożone.

  2. Niby fajnie, DRY,KISS itp. Załóżmy, że mamy klucz złożony z 3 pól na obiekcie. Oczywiście każde wymagane, ale uwaga – część z nich może się zmieniać. Jest jakiś elegancki sposób by np. w JPA/Hibernate czy innym ORMie napisać bez gimnastyki kod aktualizacji takiego tworu?
    Co jeśli w międzyczasie okaże się, że jedno pole z tych 3 jednak nie jest wymagane? Przerabiałem nawet niedawno takie sztuczki.
    Oczywiście z punktu widzenia OOD wersja z naturalnymi identyfikatorami jest zdecydowanie bardziej poprawna – tego nie neguję.
    A wsadzanie w pole daty „sztucznego” 1 stycznia 1970 jakoś mi śmierdzi.

  3. jeszcze jeden powód dla którego lepiej użyć sztucznego identyfikatora:
    Żeby użyć identyfikatora naturalnego musisz bardzo dobrze znać dziedzinę.
    Przykład:
    W bazie danych na której działam ktoś stworzył tabelę kody_pocztowe, zawierającą dwa pola: kod i miejscowość z kluczem głównym: kod-niby fajnie, ale problem w tym, że kod nie definiuje miejscowości. Przykładowo pod 07-415 jest 15 wiosek.

    Podobno pesel również nie jest unikalny.

    Pytanie, czy lepiej przeanalizować poprawność polskiego systemu nadawania peseli, czy lepiej dać już te sztuczne id. ?

    A co jeśli adres email również nie jest unikalny? Przykład może trochę wydumany: gmail upada, ktoś wykupuje domenę i zaczyna biznes od nowa. Gmail może nie upadnie, ale mały lokalny provider?

  4. @Konrad nie zgodzę się z tobą. Z dwóch powodów. Po pierwsze identyfikatory naturalne mogą być identyfikatorami liczbowymi np. numer banku. Mogą też być złożone np. numer banku + numer oddziału. Zatem jeżeli mamy do wyboru sztuczny identyfikator liczbowy i naturalny identyfikator liczbowy to nie ma znaczenia, który wybierzemy. W przypadku identyfikatorów opartych o ciągi znakowe to sytuacja jest o tyle ciekawa, że zazwyczaj z biznesowego punktu widzenia te ciągi są podstawą do wyszukiwania. Przykładowo szukając informacji o kliencie będziesz posługiwał się jego emailem/loginem, a nie bliżej nie znanym identyfikatorem. W dodatku zależy nam na ich unikalności. Swoją drogą sprawdzanie adresu email to temat na osobny wpis.

    @Michał, wszystko zależy. Jeżeli jedno z pól staje się nieobowiązkowe to znaczy, że nie było obowiązkowe od początku, zatem można bezpiecznie z niego zrezygnować. Co do aktualizowania klucza to mamy dwie wersje. Pierwsza „ręczna” gdzie aktualizujemy encje blokując ją dla innych operacji. Druga „w tle” gdzie aktualizujemy encje na podstawie warunku za pomocą procedury w bazie danych.
    Co do „sztucznego” 1 stycznia 1970, to wspomniałem o Null Object. Jest to dość niewdzięczny wzorzec, w którym głównym założeniem jest kontrakt pomiędzy użytkownikami kodu. Podobnie jak w przypadku Singletonu nieodpowiednie użycie tego wzorca prowadzi do problemów.

  5. @wojtekm, PESEL nie jest unikalny, a wynika to z pewnych historycznych zaszłości, i dlatego jak pracowałem przy tym systemie kilka lat temu to mówiło się o przejściu w PL.ID na NIP, bo ten jest unikalny. Co do przykładu z kodem pocztowym to równie dobrze można wziąć pod uwagę obie kolumny czyli kod + nazwa miejscowości. Względnie nie rozpatrywać tego przypadku jako płaskiej relacyjnej struktury danych, a jako drzewo i wykorzystać system TERYT. Nikt zresztą nie mówi, że klucz musi być prosty. Ten wpis jest raczej nakierowany na teorię niż praktykę.

  6. @Koziolek
    Ciesze się, że stałem się inspiracją tego wpisu 🙂
    Jakie jest jednak twoje złote rozwiązanie problemu? Bo opisałeś możliwe warianty, wady / zalety, ale brakuje mi konkluzji (chyba że nie umiem czytać pomiędzy wierszami).
    Czy identyfikacja na podstawie (złożonego) klucza w bazie relacyjnej (pomimo wad) jest wg Ciebie najlepsza?

  7. @Marcin, tu nie ma możliwości jednoznacznego określenia czy jest to najlepsze. Moim zdaniem warto korzystać z tego rozwiązania, bo pozwala na eliminację nadmiarowych bytów. Z drugiej strony warto przy systemach gdzie jest dużo złączeń przetestować zarówno klucze naturalne (niekoniecznie złożone) jak i sztuczne.

  8. @Koziołek

    Jeśli identyfikator naturalny jest jednokolumnowy, liczbowy, unikatowy i niezmienny, to OK. Ale we wszystkich innych przypadkach (a o nich mówisz) zaczynają się problemy.

    Druga sprawa: wyszukiwanie po identyfikatorze naturalnym. Często tak jest, zgoda, ale na złączeniach to już okrutnie bije po rozmiarze bazy i wydajności. Weźmy taki przykład. Robimy facebooka i mamy dwie tabele: user oraz photo (zdjęcia dodane przez użytkownika, wiele do jednego). Niechby nawet email był wymagany, unikatowy i niezmienny. Jaką wydajność będzie miało złączenie jeśli kluczem będzie email (varchar?), a jaką dla sztucznego ID (4-bajtowa liczba)? Jak bardzo różni się rozmiar indeksu na photo(user_id)? Przemnóż to przez kilkaset milionów użytkowników. To jest przepaść!

    Prawdopodobnie dużo szybszym rozwiązaniem jest sztuczny ID (oczywiście z indeksem) i email jako osobna kolumna (także z indeksem, skoro to popularne kryterium „biznesowe”).

    Offtop: Przydałyby się subskrypcje komentarzy na blogu, bo mnie dyskusja ominęła.

  9. Jestem za sztucznymi ID. Przez prawie 5 lat pracy w zawodzie widziałem tylko kilka projektów gdzie takie informacje jak NIP czy PESEL mogą być identyfikatorami. A pomysł żeby email był identyfikatorem uważam za poroniony (co innego uzywać emaila jako login). Wynika to z faktu że w dużej mierze są to dane osobowe i nawet bankom nie wolno tych danych zbierać ot tak sobie. GIODO niestety istnieje i sprawdza od czasu do czasu co dana firma zbiera i przechowuje. Dla niedowiarków proponuje poszukać w internecie artykułów o jak to ZTM w Warszawie wypuszczał spersonalizowane karty miejskie.
    Tak tak panowie życie weryfikuje naszą teorię.

    Co do KISS to moim zdaniem skoro prościej jest użyć sztucznego ID to czemu mamy komplikować sobie życie? Po co wielokolumnowe klucze złożone, badanie wydajności, i inne komplikację. Czy ktoś z was ma serio na to czas w pracy? Jeśli tak to tylko pozazdrościć.

  10. @Wojciech, mnie na szczęście GIODO może „skoczyć”. Email jest, jak już pisałem, ekstremalnym przykładem identyfikatora. Dziwi mnie też to, że tak wiele osób łączy identyfikatory naturalne z „wielokolumnowością”. Przy dobrym modelu wielokolumnowe identyfikatory zdarzają się stosunkowo rzadko.

  11. Brack compareTo() w klasach reprezentujących encje oznacza, że nie masz pomysłu na np. sortowanie tych encji i ich domyślne wartościowanie. W efekcie w miejscach gdzie potrzebujesz np. sortować encje w celu ich wyświetlenia implementujesz doraźne komparatory, powtarzasz kod i wprowadzasz zamieszanie.

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax