Java Love and Hate świata baz danych



Ram? Ram jest tani! Czemu programiści innych języków tak nienawidzą Javy? Oczywiście przyczyn może być wiele. Stary język ze składnią i problemami ze świata proceduralnych języków, duże zużycie pamięci przy starcie systemu, abstrakcje i fabryki piętrzące się i nie pozwalające normalnie używać bibliotek póki nie znamy wszystkich klocków i wszystko w imię wzorców. Ale nie o tym chciałbym pisać bo każdy z tych problemów został rozwiązany na jakiś sposób.

Problemem o którym chciałbym pisać jest czymś co Sławek Sobótka określa jako "abiektowość". Ta właśnie upośledzona obiektowość przejawia się przede wszystkim w getterach i setterach w każdej klasie operującej na danych. Na tej chorej idei wybudowano kolejne zło jakim jest ORM i ścisły podział na aplikacje bazo-dano-centryczne (baza danych jako źródło prawdy) i takie gdzie baza jest głupim pojemnikiem na dane a cała logika i aktualna wersja prawdy zapisana jest w pamięci aplikacji i zrzucana jedynie do bazy. Podział ten uważam za coś czego większość programistów Javy nie uświadamia sobie nawet, nie wiedzą że to istnieje. Jednocześnie wchodzą najczęściej w model koncentrujący się na aplikacji i stanie kesza hibernate jako na źródle prawdy.

Nieoptymalnie przekłada się to na bazę danych w której najpierw wyciągamy cały wiersz z bazy danych, deserializujemy do ramu po to tylko by na bazie dodać 1 + 1 i zapisać go znów. Oczywiście powiecie mi że stosujemy pole version, możemy stosować projekcje i powiedzieć by hibernate zapisywał jedynie wiersze zmienione. Jakże cholernie się to skomplikowało - czy tego nie widać? Z optymalnym zastosowaniem nie ma to wiele wspólnego i dodatkowo wchodzimy na co raz wyższe poziomy przypadkowej złożoności i programowania konfiguracją frameworka. 

Wszystko dla tego by mieć dane w pamięci. Najlepiej całą bazę danych. Pytanie jednak moje jest takie: co jest stworzone do tego by lepiej sobie radzić z operacjami na danych? Java ze swoim modelem pamięci czy bazy danych? Jeśli mamy bazy do których dostęp ma tylko jakiś mikro-serwis może to być dobre by trzymać dane tak blisko jak się da, a więc w pamięci. Pytanie czy to jest tak, że operujemy ciągle na tych samych danych, czy może mamy kilka gigabajtów danych które i tak ciągle się zmieniają bo operujemy na tabeli użytkowników, albo wystawionych fakturach w sporej korporacji.

Nie mówiąc o tym że jeśli weźmiemy ilość operacji które jesteśmy w stanie przeprowadzić nawet w PHP na bazie danych i porównamy nawet z JDBC okazuje się że zwykły PHP z jakimś podstawowym sterownikiem poradzi sobie z tym lepiej. Oczywiście możemy wsadzać swoje dane batchem. 

Problem jest w tym, że nauczyliśmy się robić rzeczy nieoptymalne, które odzwierciedlają jakieś założenia ideologiczne, modę, a na wszelkie problemy odpowiadamy tak:

Teraz żyjemy w czasach kiedy liczy się green code, kiedy płacimy za ram i za procesor i nagle to wszystko zaczyna się liczyć. Czy to znaczy że Java jest zła? Nie. Mamy całą masę ciekawych frameworków np korzystających z GraalVM i tam również można powiedzieć, że liczy się jakość kodu nie tylko rozumiana jako łatwo przyswajalne crudowe operacje na strukturach danych. Zdecydowanie lepiej Java wychodzi na używaniu najprostszych sterowników i czegoś w stylu myBatis i operowaniu na SQL w którym operacje wykonuje się metodą "set quantity = quantity -1 where quantity > 1". Nie mamy potrzeby nawet stosowania transakcji a baza zwróci nam wynik operacji byśmy wiedzieli czy operacja się udała. 

Wtedy nasz kod operując na obiektach jest w stanie komendy przetłumaczone na sql wysyłać wprost do bazy danych i nie musi wyciągać stanu by go zmieniać i może uzyskiwać zmianę atomową, i stan atomowy, bez blokowania rekordu. Wysyłamy polecenie zmiany, wiemy czy zaszła i możemy odczytać aktualną wersję wprost z bazy i nie potrzebujemy do tego nawet transakcji bo w razie niepowodzenia możemy wysłać polecenie odwrotne. Nie mamy potrzeby przechowywania wszystkiego w kosztownej pamięci. Oczywiście problemem będzie dla nas wtedy nie pamięć ale IO, ale to już temat na inny tekst o reaktywnych bibliotekach i CompletableFuture. Problem rozwiązany. Operowanie na danych poza bazą danych jest zawsze oznaką, że nie rozumiejąc istoty danych i przeznaczenia bazy danych decydujemy się na bardziej kosztowne rozwiązanie w imię źle rozumianej obiektowości.

Czy taka baza się skaluje? Sprawdźcie sami. Yugabyte, Cocroach, Spanner (cholernie drogi).

Można zapytać np. o to jak rozwiązać problem wymagania audytowalności i event sourcing. Widziałbym to prosto. Do każdej tabeli agregatu posiadamy również tabelę w której trzymamy zdarzenia systemowe wcześniej zatwierdzone przez bazę danych - ważne by trzymane były w trybie umożliwiającym jedynie dodawanie i odczyt. Ewentualne "rozjazdy" między stanem w bazie danych i wersją obiektu rekonstruowaną ze zarchiwizowanych zdarzeń są indykatorem bardzo poważnych problemów ze spójnością danych o których lepiej byśmy wiedzieli wcześniej sami. W takim przypadku pytanie pozostaje o to która wersja jest właściwa. Wzorzec mówi by ufać zawsze wersji rekonstruowanej ze zdarzeń ale jak zawsze "to zależy". 

W przypadku planowanej skalowalności i nieskalowalnej bazy danych musimy uzbroić się w coś typu Kafka która nam przetransportuje zdarzenia do innych instancji aplikacji i baz danych i tam rzeczywiście stan powinien być dopiero wtórnie traktowany jako snapshot wersji która zapewne jest nieaktualna i musi zostać aktualizowana. W przypadku użycia Kafka Connect możemy założyć, że wykorzystując strumień zdarzeń stan zostanie zaktualizowany w bazie automagicznie ale kiedy pojawi się problem ze współbieżnością, lub rebalancing możemy mieć mocno nieaktualne snapshoty. Walidacja na ich podstawie okaże się problematyczna. Walidacja i stan powstaje na podstawie zdarzeń tylko że nie możemy zakładać, że dana instancja aplikacji ma wszystkie dane które zaistniały w innych instancjach. Tu wchodzi do gry wzorzec spójności saga. Sprawy się komplikują. Znów wszystkiemu winna Java, a raczej nasza chęć rozwiązywania problemów po stronie kodu, które są już rozwiązane przez konkretne produkty. 

W przypadku kiedy mamy problemy spójności danych rozwiązane przez produkt jakim jest baza danych możemy próbować przejść na uproszczony model w którym aktualność danych rozwiązywana jest na poziomie transakcyjnej i skalowalnej bazy danych a strumień zdarzeń służy jedynie do utwierdzenia nas w przekonaniu że nie zgubiliśmy niczego i możemy odtworzyć całą historię w każdym dowolnym punkcie czasu.

Redukując tę pozorną redundancję źródła prawdy której intuicyjnie nie lubimy i zazwyczaj zamiatamy problemy związane z potencjalnym rozwarstwieniem się stanu pod dywan szczerze szeroko uśmiechając się "nic takiego się nie zadziało" i przechodząc w tym przypadku na jedno ze źródeł "prawdy" zawsze skazujemy się na wygodną dla nas ale bardzo niebezpieczną sytuację w której ścinamy fundamenty w mieszkaniu by poszerzyć sobie pokój. Pytanie tylko kiedy okaże się że jednak szalony naukowiec miał rację i na miasto spadło tornado rekinów. Może nigdy ;) Póki nie spadnie przez naszą aplikację samolot, albo nie zdarzy się coś gorszego.

Comments