Przepisz swój kod na nowo! #2

Po ostatnim przedstawieniu mojej starej aplikacji AnimalShleter biorę się za jej przepisanie na nowsze technologię. Backend będę chciał postawić w oparciu o Springa, a warstwa wizualna zostanie wyrzucona do Angulara. Dodatkowo jest w planach wystawienie usługi do wysyłki maili poprzez brokera wiadomości. Do łączenia z bazą danych będę wykorzystywał JdbcTemplate, aby ograniczyć magię Hibernate’a. Oczywiście trzeba napisać więcej kodu, ale jednak mamy większą kontrolę nad naszym kodem. Będę też wykorzystywał Liquibase do trzymania zmian schematu bazy danych oraz skorzystam z Vavr, aby pisać kod w sposób bardziej funkcyjny.

Na tą chwilę mam zamiar tylko przepisać wszystkie funkcjonalności znajdujące się w starej aplikacji. Jednak kto wie, może zacznę dodawać coś więcej w późniejszych etapach projektu. „Pożyjemy, zobaczymy”, ale na razie czas zabrać się do roboty.

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

Inicjalizacja projektu

Zacznijmy od wejścia na stronę start.spring.io, gdzie w łatwy sposób możemy sobie projekt Spring Boot’owy. Oczywiście mając Intellij w wersji Ultimate możemy to samo uzyskać z poziomu IDE.

Tworzenie projektu Spring'owego z poziomu przeglądarki
Tworzenie projektu Spring’owego z poziomu przeglądarki

Wcześniej już zaszufladkowaliśmy tą aplikację jako CRUD, więc zacznijmy od zdefiniowania endpointów. Mam świadomość, że taki CRUD można załatwić jednym interfejsem CrudRepository dostarczonym przez Springa, ale będziemy jeszcze tutaj dokładać dodatkowe funkcjonalności poza zwykłym zapisywaniem do bazy danych. No dobrze to jak będziemy mogli dostać się do naszej aplikacji.

  • GET /animals – pobiera wszystkie zwierzęta będące w schronisku;
  • POST /animals – dodaje nowego zwierzaka do schroniska;
  • GET /animals/{id} – pobiera dane zwierzaka o podanym id;
  • PUT /animals/{id} – edycja danych zwierzaka o podany id;
  • DELETE /animals/{id} – usuwa wybranego zwierzaka o podanym id;

Na razie myślę, że tyle wystarczy. Teraz zapiszmy sobie naszą podstawową tabelę, w której będziemy trzymać naszych pupili. Nazwiemy ją po prostu „animals” i na nią będą składały się następujące kolumny.

  • id – identyfikator zwierzaka;
  • name – imię dla naszego pupila;
  • kind – rodzaj zwierzaka;
  • age – wiek zwierzaka;
  • admittedAt – data, kiedy nasz pupil został przyjęty do schroniska;
  • adoptedAt – data, kiedy nasz zwierzak został adoptowany;

Postawiłem na MySQL, ponieważ z nią miałem najwięcej do czynienia, ale dla naszej aplikacji nie będzie to miało większego znaczenia. No to zabieramy się do kodzenia!

Pierwsze efekty prac

Na tą chwilę konfiguracja moich beanów wygląda następująco.

@Configuration(proxyBeanMethods = false)
public class AnimalConfig {

    @Bean
    public AnimalRepository animalRepository(JdbcTemplate jdbcTemplate) {
        return new AnimalRepositoryImpl(jdbcTemplate);
    }

    @Bean
    public AnimalService animalService(AnimalRepository animalRepository) {
        return new AnimalServiceImpl(animalRepository);
    }
}

Zdefiniowałem, że pod interfejsem AnimalRepository będę korzystał z konkretnej implementacji wymagającej do działania JdbcTemplate. Początkowo miałem też zdefiniowany bean dla JdbcTemplate, ale okazało się, że jest on zbędny. Następnie mamy zdefiniowane jaka implementacja serwisu ma być wykorzystywana w aplikacji. Do jej działania potrzebny jest obiekt AnimalRepository, dlatego podajemy go jako parametr metody. Zdecydowałem się na napisanie klasy konfiguracyjnej, aby nie oddawać całkowitej władzy Springowi. Chciałem przez to sprawdzić w jaki sposób możemy odseparować się od frameworka, czy jest to skuteczne działanie. Zachęcam Cię do zapoznania się z moim wpisem, który rozprawia nad różnicami pomiędzy @Bean a @Component.

Dodatkowo wykorzystałem też profilowanie aplikacji dzięki czemu przez podanie parametru -Dspring.profiles.active=dev jestem w stanie odczytywać właściwości deweloperskie. Robi się to naprawdę prosto, wystarczy zdefiniować plik o nazwie application-dev.properties, czyli wartość po myślniku musi odpowiadać wartości podanej w parametrze.

Wraz z wykorzystaniem Liquidbase jestem w stanie spisywać wszelkie zmiany jakie zachodzą w schemacie bazy danych. Na tą chwilę plik db.changelog-master.yml wygląda następująco.

databaseChangeLog:
  - changeSet:
      id: create-table-animals
      author: devcezz
      preConditions:
        - onFail: MARK_RAN
          not:
            tableExists:
              tableName: animals
      changes:
        - createTable:
            columns:
              - column:
                  autoIncrement: true
                  constraints:
                    nullable: false
                    primaryKey: true
                    primaryKeyName: animal_pkey
                  name: id
                  type: BIGINT
              - column:
                  constraints:
                    nullable: false
                  name: name
                  type: VARCHAR(250)
              - column:
                  constraints:
                    nullable: false
                  name: kind
                  type: VARCHAR(250)
              - column:
                  constraints:
                    nullable: false
                  name: age
                  type: BIGINT
            tableName: animals

  - changeSet:
      id: add-date-tables-to-animals
      author: devcezz
      preConditions:
        - onFail: MARK_RAN
          tableExists:
            tableName: animals
      changes:
        - addColumn:
            tableName: animals
            columns:
              - column:
                  constraints:
                    nullable: false
                  name: admittedAt
                  type: DATETIME
                  defaultValueDate: CURRENT_TIMESTAMP
              - column:
                  name: adoptedAt
                  type: DATETIME

Zdecydowałem się na pisanie w YAML a nie XML, ponieważ chciałem spróbować czegoś z czym jestem mniej obeznany. Dodałem też dwa changesety, aby sprawdzić jakie mechanizmy można wykorzystywać w Liquidbase. W pracy też wykorzystujemy tą bibliotekę, jednak w wersji XML oraz nie miałem okazji uczestniczyć w dodawaniu tego narzędzia. Wydaje mi się, że warto się zapoznać z tym rozwiązaniem, ponieważ mamy spisaną całą historię zmian bazy danych. Wiemy przez to jakie decyzje były podejmowane podczas dewelopmentu.

Struktura projektu

Odświeżona struktura projektu AnimalShelter
Odświeżona struktura projektu AnimalShelter

Postanowiłem podzielić aplikację na dwa moduły Mavena. Pierwszy z nich będzie zawierał tylko logikę biznesową, która będzie wolna od jakichkolwiek frameworków. Drugi natomiast będzie oparty o infrastrukturę Springa i będzie wykorzystywał funkcjonalności dostarczone przez ten pierwszy moduł. Jak na razie nie udało mi się tego jeszcze tak rozdzielić, ale jestem na dobrej drodze.

Dodatkowo widać w np. w pakiecie pl.csanecki.animalshelter.service znajduje się implementacja serwisu oraz niezbędny dla niej interfejs repozytorium. Wyszedłem z założenia, że w ten sposób AnimalShelterImpl nie jest uzależniona od żadnej konkretnej klasy. Ona tylko komunikuje jakiej zależności potrzebuje i to my jesteśmy zobligowani do jej zapewnienia.

Oczywiście zapomniałem dodać, że zdecydowałem się na najpowszechniejszą architekturę, czyli MVC. Składają się na nią poprzeczne warstwy, gdzie będziemy mieli kontrolery, logikę biznesową, persystencję oraz bazę danych.

Aktualne działanie aplikacji

Aktualnie wysyłając zapytanie POST na localhost:8080/animals z podanym niżej body.

{
    "name": "Płotka",
    "kind": "Mysz",
    "age": 1
}

Uzyskamy odpowiedź 201 z zawartością.

{
    "id": 3,
    "name": "Płotka",
    "kind": "Mysz",
    "age": 1,
    "admittedAt": "2020-12-05 15:02:47",
    "adoptedAt": null
}

Uderzając zapytaniem GET na ten sam adres localhost:8080/animals uzyskamy listę wszystkich zwierzaków w schronisku ze statusem 200.

[
    {
        "id": 1,
        "name": "Karmel",
        "kind": "Pies",
        "age": 3,
        "admittedAt": "2020-12-05 15:02:14",
        "adoptedAt": null
    },
    {
        "id": 2,
        "name": "Judy",
        "kind": "Kot",
        "age": 2,
        "admittedAt": "2020-12-05 15:02:36",
        "adoptedAt": null
    },
    {
        "id": 3,
        "name": "Płotka",
        "kind": "Mysz",
        "age": 1,
        "admittedAt": "2020-12-05 15:02:47",
        "adoptedAt": null
    }
]

Jeśli chcemy uzyskać dane wybranego zwierzaka musimy także wywołać GET tylko już na adreslocalhost:8080/animals/{id} podając w ścieżce ID konkretnego pupila. Gdy podamy ID, którego nie ma w bazie to uzyskamy status 404, natomiast, gdy dany zwierzak istnieje w systemie dostaniemy 200 wraz z ciałem odpowiedzi.

{
    "id": 2,
    "name": "Judy",
    "kind": "Kot",
    "age": 2,
    "admittedAt": "2020-12-05 15:02:36",
    "adoptedAt": null
}

Podsumowanie

To na razie tyle ile udało mi się uzyskać na ten moment. Będę dalej rozwijał aplikację w pierwszej kolejności zajmując się dokończeniem wszystkich endpointów oraz starając się dokonać lepszej segregacji klas. Dziękuję bardzo za to, że ze mną jesteś i będę wdzięczny jeśli podzielisz się komentarzem z opinią co sądzisz o tej serii.

Oczywiście załączam też link do mojego repozytorium z aktualnym projektem: AnimalShelterNew.

Podziel się tym z innymi!

Może Ci się również spodoba