Zaczynając pracę nad Star Trek API poszukiwałem narzędzia, które pozwoliłoby na elastyczną konfigurację wieloetapowego procesu zasilenia bazy danych danymi z wielu źródeł. Nie znałem takiego narzędzia z pierwszej ręki, ale chcąc uniknąć eksperymentów i konieczności wycofywania się z raz podjętej decyzji, postawiłem na najpopularniejsze dostępne rozwiązanie – Spring Batch, i jak dotąd, sprawdza się on bardzo dobrze, z wyjątkiem dwóch niedogodności, o których napiszę na koniec.
Spring Batch dostarcza ogólny szkielet, który wypełniamy naszą logiką biznesową. Na najwyższym poziomie w Spring Batchu znajdują się joby, których możemy mieć wiele i które mogą być wykonywane sekwencyjnie lub współbieżnie. W każdym jobie znajduje się wiele kroków, które również mogą być wykonywane sekwencyjnie lub współbieżnie. Kroki mogą być restartowalne, wykonywane raz, lub mieć limit wykonań. W moim przypadku pożądanym było, by każdy krok wykonywał się raz, ponieważ przyjąłem założenie, że baza będzie każdorazowo zasilana od nowa. Dopiero w przyszłości zaimplementuję zasilanie przyrostowe, które będzie ograniczone do tych danych źródłowych, które zmieniły się od ostatniego zasilenia.
Krok w Spring Batchu składa się z readera, procesora i writera. Reader, zależnie od implementacji, ładuje wszystkie dane przez rozpoczęciem kroku, lub doładowuje je przed wywołaniem procesora. Procesor implementuje interfejs ItemProcessor<I, O>
, gdzie I
to typ obiektu, od którego zaczyna się przetwarzanie, a O
to typ zwracanego obiektu. Procesor ma tylko jedną metodę o sygtaturze O process(I item)
. Procesory mogą być springowymi beanami i dzięki temu logika wewnątrz pojedynczego procesora może być dowolnie rozbudowana. Ostatnim elementem kroku jest writer, który zapisuje wynikowe obiekty. Mogą być one zapisane gdziekolwiek, np. na dysk lub do jakiegoś cache’u, do użycia w jednym z kolejnych kroków, ale w przypadku startrekowego API naturalnym jest, by przetworzone encje trafiały do bazy danych.
Pojedynczy krok w Star Trek API najczęściej wiąże się z zasileniem pojedynczej tabelki w bazie danych. Aby stworzyć krok, najpierw trzeba wyznaczyć grupę kategorii, w których znajdują się strony mogące się przełożyć na wiersz w tabeli. Przykładowo, taką kategorią, od której może zacząć się krok, są Companies, czyli firmy, które pracowały nad Star Trekiem. Następnie do pamięci ładowane są nagłowki stron z danej kategorii. Nagłówki zawierają ID strony, jej nazwę, oraz źródło – Memory Alpha lub Memory Beta. Na tym kończy się reader. Następnie, już w procesorze, na podstawie takich nagłówków jest pobierana cała treść strony, i wóczas wykonywane jest przetworzenie w sposób właściwy dla danej encji, które kończy się przemapowaniem encji reprezentującej szablon z wiki na encje bazodanową. Szablony to infoboxy, które na pewno każdy widział na Wikipedii po prawej stronie wielu stron. Na koniec mamy zapis, które obejmuje dodatkowo odfiltrowanie duplikatów. Duplikaty biorą się z faktu, że na wiki istnieją przekierowania, co powoduje, że dwa różne nagłówki stron mogą wskazywać na tę samą stronę.
Kolejne kroki przetwarzające encje są konfigurowalne przez javowe propertisy. To już funkcjonalność, którą dopisałem sam. Każdy krok można uruchomić oddzielnie, lub można uruchomić kilka wybranych kroków, a inne wyłączyć. Kroki nie posiadają między sobą zależności i można je wykonywać w dowolnej kolejności i jako dowolny podzbiór. Zależności istnieją tylko między encjami bazodanowymi. Jeśli, przykładowo, chcę przetworzyć komiksy bez przetworzenia wpierw firm, wówczas wszystkie komiksy będą miały wydawcę ustawionego na null, ale poza tym zostaną z powodzeniem zapisane.
Żeby już nie duplikować kodu na blogu, przykład tego, jak wygląda reader, processor i writer można zobaczyć tutaj.
A teraz o dwóch niedogodnościach.
Jedną niedogodnością, jaką napotkałem, wynikającą być może z początkowej nieznajomości Spring Batcha, była konieczność dwukrotnej zmiany formatu konfiguracji. Najpierw napisałem konfigurację javową, ale po jakimś czasie została ona wymieniona na konfigurację XML-ową z powodu trudności w testowaniu Javy, oraz dlatego, że więszkość tutoriali, na które natrafiałem, używała notacji XML-owej. Trzy tygodnie później doszedłem do wniosku, że XML-owa konfiguracja nie daje się łatwo konfigurować za pomocą propertisów, i ponownie wymieniłem ją na javową, tym razem przykładając się do napisania pełnych testów.
Drugą niedogodnością, wynikającą być może z chęci stworzenia zbyt wysokopoziomowego interfejsu przez autorów Spring Batcha, jest niemożność łatwego dostania się do repozytoriów, które odpowiadają za operacje na jobach i stepach. Potrzebowałem tego, żeby nie ładować danych do już zakończonych kroków, bo czasami może to trwać kilka minut, a ładowanie danych odbywa się w momencie tworzenia springowych beanów, czyli efektywnie w trakcie startu aplikacji. Po dłuższym grzebaniu w kodzie doszedłem do rozwiązania, które na pewno nie spodobało by się autorom Spring Batcha. Można je obejrzeć tutaj. Sprowadza się ono do odpakowania klasy SimpleJobRepository ze springowego proxy, a następnie do wystawienia kilku jego prywatnych pól, wydobytych refleksją, jako beany. Brzydkie rozwiązanie, ale działa.