Ostatnio sporo czasu poświęciłem kodowaniu aplikacji AnimalShelter. Udało mi się wykonać naprawdę sporo zadań, ale nie obeszło się bez problemów, którymi chciałbym się z Tobą podzielić w tym wpisie. Na pewno przedstawię nowy podział na moduły Mavena jakiego dokonałem, co dzieje się z UUID w MySQL, przygotowanie przesyłania plików pomiędzy serwisami oraz na którą bibliotekę do konwersji HTML na PDF się zdecydowałem.

Lista wszystkich wpisów dotyczących projektu AnimalShelter:
#1 - Opis projektu AnimalShelter
#2 - Pierwsze kroki w backendzie
#3 - Refactoring i prace rozwojowe części serwerowej
#4 - Tworzenie GUI w Angularze
#5 - Zatrzymaj się, przemyśl i zacznij działać!
#6 - Pomysł na architekturę
#7 - Wykorzystanie CQRS
#8 - Ponowna implementacja
#9 - Rozterki architektoniczne
#10 - Podsumowanie + implementacja wysyłki maili
#11 - Programowania ciąg dalszy
#12 - Dopinanie zadań do końca

Nowy ład w modułach

Podczas pracy nad AnimalShelter naszła mnie pewna myśl. Skoro projektuje kod w taki sposób, aby poszczególne konteksty były w oddzielnych modułach Mavena to na pewno będą musiały być one od siebie zależne. Tylko czy chcę, aby każdy moduł pobierał cały kod innego modułu? Uznałem, że niekoniecznie i z tego powodu postanowiłem coś z tym zrobić. Rozdzieliłem, więc każdy z modułów na dwa oddzielne - główną logikę oraz model. Oczywiście kod z logiką biznesową będzie zawierał zależność do swojego modelu, aby móc zwracać klasy z niego w swoich metodach.

Schemat zależności modułów w AnimalShelter
Schemat zależności modułów w AnimalShelter

W ten sposób moduł od notyfikacji może mieć tylko zależności do modelu zewnętrznego modułu schroniska czy administracyjnego, aby obsłużyć wysyłane zdarzenia. Moduł webservice natomiast zawiera odwołania do kodu logiki biznesowej innych modułów, aby móc je zarejestrować w kontekście Springa. Być może całe to rozdzielenie to niekoniecznie jest właściwe rozwiązanie, bo wprowadza narzut w postaci wielu modułów Mavena. Jednak warto było to sprawdzić na swoim pet project. W tym miejscu chciałbym zapytać Ciebie co Ty sądzisz o takim pomyśle?

UUID jako typ kolumny

W MySQL istnieje typ kolumny UUID. Jeśli go wybierzemy dostaniemy w zamian tak naprawdę VARCHAR(36), czyli tyle znaków ile zawiera właśnie UUID np. ‘bcc89ed1-1cdb-4e14-9875-2cb30d92bbd9’. Jednak po lekturze artykułu na blogu Philipp’a Hauer’a chciałem wypróbować podejście wykorzystujące BINARY(16). W tym celu trzeba dokonać pewnych transformacji na UUID - usunąć wszystkie myślniki i zamienić na postać binarną. Dzięki takiemu podejściu UUID na pewno będzie o wiele wydajniejszy jako klucz główny. Wtedy możemy go użyć w tym celu w klasie encji oznaczonej @Entity.

1
2
3
4
5
@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
@Column(columnDefinition = "BINARY(16)")
private UUID id;

Problem pojawia się natomiast, gdy sami chcielibyśmy wrzucić coś do bazy np. przy wykorzystaniu JdbcTemplate. Musimy się sporo nakombinować, aby wszystko działało jak należy. Jeśli bez żadnych transformacji wrzucimy UUID w zapytaniu to otrzymamy oczywisty błąd - “com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column ‘zookeeper_id’ at row 1”. Musimy przy dodawaniu rekordu zmienić UUID usuwając myślniki i wywołując metodę unhex() - unhex(replace(uuid(), '-', '')). O ile to rozwiązanie da się uzyskać przy dosyć małym nakładzie dla wrzucania danych do bazy to dla odczytu może to nie być takie proste. Należy na zapisanym kluczu wywołać metodę hex() oraz wstawić w odpowiednie miejsca brakujące myślniki.

1
2
3
4
5
6
7
8
9
public void save(ZookeeperContact zookeeperContact) {
  jdbcTemplate.update("" +
          "INSERT INTO zookeeper_contact " +
          "(zookeeper_id, email) VALUES" +
          "(unhex(replace(?, '-', '')), ?)",
      zookeeperContact.zookeeperId().value().toString(),
      zookeeperContact.email()
  );
}

Właśnie z tego powodu zdecydowałem się na trzymanie UUID tak samo jak do tej pory, czyli w VARCHAR(36). Może faktycznie jest to mniej wydajne, ale dla mojej aplikacji nie ma to większego znaczenia. Kod jest po prostu prostszy, bez żadnych transformacji, jedynie w jednym miejscu trzeba wywołać metodę toString(). Dodatkowo w encji Zookeeper będę musiał trzymać id jako String a nie UUID, żeby nie otrzymać błędu - "java.sql.SQLException: Incorrect string value: '\xB8\xEF7\xB5F\xB8…' for column 'zookeeper_id' at row 1". Fajnie wiedzieć jednak, że można dokonać takiej optymalizacji, gdy zajdzie taka konieczność.

Oczywiście można jako klucz główny trzymać Long, a dodatkowo mieć kolumnę UUID, aby wiedzieć czy dana encja, w rozumieniu DDD, jest unikalna. W ten sposób otrzymujemy wydajniejszy indeks oraz łatwy sposób na odróżnienie dwóch obiektów, oczywiście kosztem zajmowanego miejsca.

Przesyłanie plików pomiędzy dwoma mikroserwisami

W tej chwili operuję na architekturze modularnego monolitu, a przynajmniej mam taką nadzieję. Jednak jak pisałem wcześniej chciałbym mieć możliwość “bezproblemowego” przejścia na mikroserwisy dla ewentualnego treningu. Pojawiło się też wymaganie, aby móc generować pliki PDF oraz CSV z aktualną listą pupili w schronisku. Postanowiłem, więc stworzyć nowy moduł, którego odpowiedzialnością będzie właśnie tworzenie takich raportów. Postawiłem na taki krok, ponieważ te pliki będzie można pobrać bezpośrednio z aplikacji lub znajdą się one w załączeniu do wysyłanych maili. Czyli istnieją dwa sposoby wykorzystania tego mechanizmu.

W przypadku monolitu ten problem jest trywialny do rozwiązania, ponieważ możemy pomiędzy modułami przesłać ścieżkę do wygenerowanego raportu. Jednak co w przypadku, gdy chcemy mieć możliwość późniejszego wydzielenia serwisów? Wpadłem na pomysł, że będę po prostu już na tym etapie przesyłał tablicę bajtów. Jest to spowodowane tym, że w Spring, aby wysłać plik możemy wykorzystać ByteArrayResource zgodnie z wpisem na StackOverflow. Następnie wykorzystując RestTemplate istnieje możliwość bezproblemowego pobrania go w takiej postaci. Na ten moment jest to na pewno overengineering, ale chciałem w ten sposób dostosować się do przyszłych wymagań (które sam sobie stawiam). Co myślisz o takim rozwiązaniu?

Generowanie PDF na podstawie HTML

Szukałem rozwiązania w Internecie, które pozwoliłoby mi wygenerować plik PDF na podstawie wcześniej zdefiniowanego szablonu HTML. Udało się takie znaleźć! Na początku wykorzystuję Thymeleaf, aby wygenerować HTML uzupełniony o niezbędne dane np. listę zwierząt. Następnie wytworzony rezultat poddawany jest obróbce przez kolejną bibliotekę. Na pierwszy ogień poszedł projekt Flying Saucer, który poleca Baeldung. Faktycznie wszystko działało jak należy do czasu, gdy nie pojawiły się polskie znaki. O co chodzi? W przypadku dodania pupila o imieniu “Piątek” zwróciłem uwagę, że w PDF widnieje on jako “Pitek”. Od razu dodałem też zwierzaka o nazwie “ŻÓŁĆ” czego wynikiem był “Ó”. Próbowałem znaleźć rozwiązanie tego problemu. Oczywiście pomocny okazał się niezawodny StackOverflow. Jednak to rozwiązanie wydaje się dosyć rozwlekłe i w moim przypadku nawet nie zadziałało, ale to raczej z mojej winy (miałem ustawione font-family dla tabelki czego nie uwzględniłem).

Wznowiłem, więc poszukiwania kolejnej biblioteki. Tym razem trafiłem na projekt iTextPdf i to był strzał w dziesiątkę! Interfejs do uzyskania pliku PDF jest naprawdę prosty. Wystarczy przekazać do metody HtmlConverter.convertToPdf HTML w postaci String oraz OutputStream wskazujący na miejsce, gdzie ma być zapisany rezultat. Przy przyjrzeniu się powstałemu PDF moje oczy ujrzały prawidłowe nazwy - “Piątek” oraz “ŻÓŁĆ”. Problemem było jedynie to, że polskie znaki występowały w innej czcionce niż reszta. Rozwiązaniem było wykorzystanie obiektu klasy ConverterProperties i przekazanie go jako trzeci argument do HtmlConverter.convertToPdf. Dodatkowo w HTML warto ustawić CSS wskazujący na wybraną przez nas czcionkę - body{font-family:Calibri,serif;}. W ten sposób uzyskałem to co sobie założyłem - PDF powstały z HTML wraz z niezbędnymi danymi na temat schroniska.

Przykład wygenerowanego raportu PDF na temat schroniska
Przykład wygenerowanego raportu PDF na temat schroniska

Problem z parent w Maven

Jeżeli korzystamy z dobrodziejstw Spring Boot i prowadzimy projekt korzystając z Maven to rodzicem dla naszego modułu będzie spring-boot-starter-parent. Jednak jeżeli zdefiniowaliśmy sobie swój moduł root to inne nasze podmoduły mogą z niego korzystać, aby odziedziczyć po nim właściwości. Co w przypadku, gdy jednym z podmodułów jest właśnie moduł oparty o Spring Boot? Nie może on skorzystać z naszego modułu root, więc wszystkie właściwości czy pluginy muszą być w nim zdublowane. Przynajmniej na mój obecny stan wiedzy. Taką sytuację mam właśnie u siebie z modułem webservice. Może masz jakiś pomysł jak ten problem rozwiązać? Z chęcią zapoznam się z potencjalnymi pomysłami.

Podsumowanie

Kodowanie mojego projektu naprawdę ostatnio mnie wciągnęło. Muszę przyznać, że pozostało mi jeszcze dodanie generowanie pliku CSV oraz stworzenie interfejsu użytkownika w Angularze, który rozpocząłem już kiedyś. Być może również pokuszę się o stworzenie frontendu w Vaadin, ale zobaczymy co czas pokaże. Na ten moment mam nadzieję, że moje doświadczenia przydadzą się Tobie podczas tworzenia własnego projektu. Oczywiście z chęcią zapoznam się z Twoją historią oraz wyzwaniami z jakimi przyszło Ci się zmierzyć podczas programowania! Zachęcam przy okazji do spojrzenia na aktualną listę zadań na Github.