CodeGym /Kursy /Spring Data JPA /Pierwsze spotkanie ze Spring Data

Pierwsze spotkanie ze Spring Data

Spring Data JPA
Poziom 1, Lekcja 0
Dostępny

1. Zielony Postman jeszcze nic nie znaczy 😅

Jeśli aplikacja potrafi przyjąć żądanie, coś policzyć i zwrócić odpowiedź, wygląda przekonująco. Szczególnie na demo. Ale biznes niemal nigdy nie ma scenariuszy w stylu „policz i zapomnij". Biznes żyje faktami, które muszą przetrwać w czasie: produkt istnieje w katalogu, stan magazynowy się zmienił, zamówienie zostało złożone, e-mail klienta jest powiązany z zamówieniem, cena w zamówieniu jest zablokowana na moment zakupu. To już nie są „dane na jedno wywołanie", a stan systemu.

Różnica między demo a prawdziwym backendem sprowadza się zwykle do bardzo praktycznego pytania: czy za godzinę, dzień po dniu i po restarcie można te dane ponownie odczytać? Jeśli nie, to mamy przed sobą na razie nie warstwę przechowywania, a jedynie pamięć procesu. Może być przydatna, szybka i wygodna, ale biznes nie może się na niej opierać 🚫

Warto mieć w głowie jedną dojrzałą myśl. Backend staje się prawdziwym nie w momencie, gdy uruchomisz serwer HTTP, lecz wtedy, gdy system zacznie utrzymywać fakty biznesowe jako trwały stan. Właśnie dlatego kurs o persistence jest potrzebny osobno, a nie jako jeden rozdział w stylu „no i jest tam jeszcze repozytorium".

2. Pamięć procesu jest wygodna, ale zawodna 😬

Obiekty i kolekcje Javy są świetne do obliczeń. To ich naturalne środowisko: stwórz obiekt, wrzuć do List, przefiltruj, posortuj, zwróć wynik. Problem zaczyna się nie podczas obliczeń, lecz w momencie, gdy temu wynikowi nadaje się zbyt głośną nazwę. Wrzucenie stringa do List nie oznacza jeszcze „zapisania produktu". To jedynie wrzucenie stringa do pamięci bieżącego procesu.

Spójrzcie na uproszczony przykład:

import java.util.ArrayList;
import java.util.List;

// Serwis przechowujący dane tylko w pamięci procesu (wewnątrz JVM)
class InMemoryCatalogService {
    // "Baza" w formie zwykłej listy: żyje tak długo, jak żyje obiekt/proces
    private final List<String> products = new ArrayList<>();

    void add(String name) {
        // Dodajemy element do pamięci operacyjnej, a nie do trwałego magazynu
        products.add(name);
    }

    List<String> findAll() {
        // Zwracamy kopię, żeby zewnętrzny kod nie mógł przypadkowo zmodyfikować wewnętrznej listy
        return List.copyOf(products);
    }
}

Ten kod nie jest „zły". Uczciwie robi to, co obiecuje: przechowuje listę produktów wewnątrz obiektu. Do testu, demonstracji lub krótkotrwałego obliczenia to całkiem dobre rozwiązanie. Ważne jednak, żeby nie przypisywać mu nadmiernej niezawodności. Przechowuje dane nie „w systemie", lecz jedynie wewnątrz bieżącej JVM.

Gdy tylko kończy się życie procesu, kończy się też życie tej listy. To nie defekt Javy i nie „ograniczenie właśnie ArrayList". Tak działa pamięć procesu: żyje dokładnie tyle, ile żyje sam proces. Aplikacja zatrzymana, kontener zrestartowany, IDE zamknięte, serwer padł — i po wszystkim, pamięć wyzerowana 🔌

Dlatego zdanie „przecież już zapisałem do List" brzmi pewnie tylko do pierwszego restartu. Po nim okazuje się, że List był kartką z notatkami, a nie archiwum systemu.

3. Nowe żądanie, nowa instancja, nowa prawda?

Nawet zapominając o restartach, pozostaje jeszcze jeden problem: backend nie żyje jedną metodą i jednym użytkownikiem. Żądania HTTP przychodzą niezależnie od siebie. Same z siebie nie tworzą żadnego magicznego wspólnego stanu. A jeśli aplikacja działa w kilku instancjach, każda ma własną pamięć.

Można to poczuć nawet na zabawkowym przykładzie:

// Dwie "instancje" serwisu: w rzeczywistości mogłyby to być dwa egzemplarze aplikacji
InMemoryCatalogService instanceA = new InMemoryCatalogService();
InMemoryCatalogService instanceB = new InMemoryCatalogService();

instanceA.add("Tea"); // Dodaliśmy produkt tylko do pamięci instanceA

System.out.println(instanceA.findAll()); // [Tea] — w tej instancji produkt jest
System.out.println(instanceB.findAll()); // [] — w drugiej instancji pamięć własna, produktu nie ma

Tutaj dwa obiekty odgrywają rolę dwóch oddzielnych instancji aplikacji. Jedna ma własną pamięć, druga — swoją. Dla katalogu, stanów magazynowych i zamówień to katastrofa, bo biznes zazwyczaj chce jednej prawdy, a nie dwóch niespójnych wersji rzeczywistości 😬

Właśnie dlatego rozmowa o danych szybko opiera się nie tylko o kwestię „czy aplikacja przeżyje restart", ale i o „czy jest jedno źródło prawdy dla wszystkich żądań i wszystkich instancji aplikacji". Jeśli takiego źródła nie ma, system zaczyna kłamać w najbardziej nieprzyjemnych miejscach: zamówienie niby zostało złożone, ale potem nie można go znaleźć; stan magazynowy niby zmniejszony, ale druga instancja o tym nie wie; produkt niby jest w katalogu, ale tylko na jednym serwerze.

Tutaj backend przestaje być „zestawem handlerów żądań" i zaczyna stawać się systemem. System potrzebuje jednego źródła prawdy. W tym kursie takim źródłem będzie PostgreSQL, a cały otaczający kod — to właśnie przyszła warstwa persistence.

4. static nie ratuje

Kolejna popularna próba „naprawienia" problemu — zrobić pole static. Logika jest zrozumiała: skoro static, to wspólne; skoro wspólne — to żyje dłużej; skoro żyje dłużej — to prawie baza danych. Nie. static nie czyni przechowywania trwalszym. To po prostu wspólna pamięć wewnątrz tego samego procesu.

Jeśli napisać tak:

class OrderNumberGenerator {
    // Pole static wspólne dla wszystkich obiektów w ramach JEDNEGO procesu JVM
    private static int nextNumber = 1;

    static int next() {
        // Zwracamy bieżącą wartość i zwiększamy ją
        // Po restarcie aplikacji nextNumber znów będzie 1
        return nextNumber++;
    }
}

to wewnątrz jednego uruchomienia aplikacji wszystko rzeczywiście będzie wyglądać lepiej. Różne obiekty zobaczą tę samą wartość licznika. Ale wystarczy, że proces się zrestartuje, i generator znów zacznie od 1. Jeśli aplikacja działa w dwóch instancjach, każda będzie miała swój własny „globalny" licznik. Wychodzi nie jedna prawda, lecz dwa lokalne mity.

static ma też inny problem. Nie tylko nie daje durable state, ale i sprawia, że stan jest trudniejszy w zarządzaniu. Globalną pamięć trudniej testować, trudniej izolować i częściej prowadzi do nieoczekiwanych efektów ubocznych. Czyli nie tylko nie rozwiązałeś persistence, ale jeszcze uczynisz kod bardziej kapryśnym.

Z tego płynie dobry praktyczny wniosek: static to głośniejsza pamięć, ale nie niezawodniejsza pamięć. Nie zamienia danych tymczasowych w trwałe. Jedynie nieco rozszerza zasięg tymczasowej iluzji.

5. Wartość tymczasowa a fakt biznesowy

Bardzo przydatne jest rozróżnianie dwóch typów danych. Pierwszy typ — obliczenia tymczasowe. Na przykład policzyłeś sumę pozycji zamówienia, zastosowałeś rabat, zebrałeś pośrednią listę, przefiltrwałeś wartości. To wszystko ważne rzeczy, ale nie muszą istnieć w systemie same w sobie. Mogą żyć sekundę i znikać.

Drugi typ — fakty biznesowe. To dane, do których system i biznes będą się później odwoływać: zamówienie nr 1037 istnieje, klient zamówił dwie pozycje, stan magazynowy zmniejszył się o 2, cena w zamówieniu wynosiła 19.99, a nie bieżąca cena katalogowa. Te dane muszą być nie tylko obliczone, ale i utrwalone.

Dla porównania spójrzcie na taką metodę:

import java.math.BigDecimal;

class OrderMath {
    BigDecimal calcLineTotal(BigDecimal unitPrice, int quantity) {
        // obliczamy sumę pozycji (cena * ilość)
        return unitPrice.multiply(BigDecimal.valueOf(quantity));
    }
}

To przydatny kod. Ale nie tworzy faktu biznesowego. Jedynie oblicza wartość. Sam w sobie fakt obliczenia nie oznacza, że gdzieś pojawiło się zamówienie albo że system jest teraz zobowiązany pamiętać tę sumę jutro.

Tutaj przebiega bardzo ważna granica. „Policzyliśmy sumę zamówienia" i „zapisaliśmy zamówienie" — to nie to samo. Dopóki danych nie można ponownie odczytać jako stanu systemu, to jeszcze nie persistence. To jedynie krótki epizod wewnątrz metody.

Dlatego w backendzie tak ważne są nazwy. Jeśli każde add do List nazywasz save, mózg szybko przestaje odróżniać tymczasowe obliczenie od durable state. A potem JPA wydaje się magią, choć problem zaczął się dużo wcześniej — na poziomie niedokładnego myślenia.

6. Co tak naprawdę robi warstwa persistence 💾

Teraz można dać definicję bez akademickiej ciężkości. Warstwa persistence — to część aplikacji odpowiedzialna za to, by fakty biznesowe zamieniały się w trwały stan i mogły być potem ponownie odczytane, zaktualizowane lub usunięte. To nie po prostu „baza gdzieś leży", a konkretna strefa odpowiedzialności wewnątrz backendu.

Ważne, żeby nie mylić trzech rzeczy. PostgreSQL — to magazyn. JDBC, JPA, Hibernate i Spring Data JPA — to sposoby pracy z tym magazynem na różnych poziomach. A warstwa persistence — to inżynierska strefa odpowiedzialności, gdzie aplikacja uzgadnia sama ze sobą, jak zamieniać dane domenowe w trwały stan i z powrotem.

Właśnie z tego później pojawią się repozytoria, model encji, transakcje, wygenerowany SQL, ograniczenia schematu i testy. Ale na razie wystarczy dostrzec jedną rzecz: warstwa persistence — to nie „dodatek do aplikacji", a warstwa oddzielająca prawdziwy system od tymczasowego dema.

Jest też uczciwa granica kursu. Nie będziemy udawać, że mówimy dziś o całym backendzie naraz. Ten kurs nie dotyczy kontraktów webowych, security ani mikroserwisów. Dotyczy jednego serwisu Spring Boot z jedną bazą danych, w którym data-layer jest zrobiony profesjonalnie. Właśnie taki fokus sprawia, że nauka jest użyteczna, a nie rozlewa się na wszystkie strony.

7. Zestaw czterech pytań

Żeby nie spierać się za każdym razem abstrakcyjnie, wygodnie jest zabrać ze sobą prostą diagnostyczną soczewkę. Można ją stosować do dowolnych danych w backendzie: zamówienia, stanu magazynowego, numeru dokumentu, cache'u, pośredniego obliczenia.

Soczewka „to już persistence czy jeszcze nie?"

Pytanie Jeśli odpowiedź brzmi „nie" Praktyczny wniosek
Czy te dane przeżyją restart aplikacji? Znikną razem z procesem To nie jest trwałe przechowywanie
Czy te dane tak samo widoczne będą dla różnych instancji aplikacji? Każda będzie miała własną wersję To nie jest jedno źródło prawdy
Czy te dane można ponownie odczytać za godzinę lub jutro? Nie można ich przywrócić jako faktu systemu To jeszcze nie fakt biznesowy w magazynie
Czy na tych danych może bezpiecznie oprzeć się scenariusz biznesowy? Błąd lub restart wszystko zniszczy Za wcześnie nazywać to „zapisem"

Zachowajcie tę tabelę. Jest mała, ale bardzo robocza. I tak, z niej wyrasta niemal cały kurs. Gdy tylko dane muszą przetrwać czas, żądania i wiele instancji, od razu ważne stają się model przechowywania, granice warstwy danych i to, jak aplikacja w ogóle komunikuje się z bazą danych.

Krótka formuła do zapamiętania też jest przydatna: pamięć to akcelerator, a nie sejf. Cache w pamięci — można. Opierać się na niej jako jedynej kopii zamówienia — nie można 🛑

8. Projekt mini-shop 🛒

Teraz powiążmy tę logikę z projektem kursu. W mini-shop mamy trzy strefy: katalog, stany magazynowe i zamówienia. To nie przypadkowy zestaw encji, a trzy grupy danych, na których backend bardzo szybko sprawdza się pod względem dojrzałości.

Jeśli katalog żyje tylko w pamięci, sklep „zapomina", co sprzedaje. Jeśli stany magazynowe żyją tylko w pamięci, dwóch użytkowników może kupić ten sam ostatni produkt. Jeśli zamówienia żyją tylko w pamięci, klient złożył zamówienie — a po restarcie systemu supportowi nagle nie ma nic do pokazania.

Można to zebrać w bardzo przyziemny obrazek:

Co przechowuje system Co się psuje, jeśli trzymamy to tylko w pamięci
Kategorie i produkty Po restarcie sklep wygląda jakby był pusty
Stany magazynowe produktów Różne instancje widzą różne ilości
Zamówienia i pozycje zamówienia Zamówienie „było", ale potem zniknęło bez śladu

I właśnie tutaj ujawnia się prawdziwy zamysł całego kursu. Nie będziemy „uczyć się korzystać z repozytoriów", a budować warstwę, która potrafi niezawodnie pracować właśnie z takimi danymi: odczytywać je, zmieniać, zachowywać spójność i nie zamieniać wszystkiego w zbiór przypadkowych wywołań save.

9. Typowe błędy startu 🚧

Błąd nr 1: nazywanie danych w List zapisanymi.
Jeśli dane żyją tylko wewnątrz bieżącego procesu, lepiej mówić „trzymamy w pamięci", „cachujemy" lub „tymczasowo przechowujemy". Słowo „zapisaliśmy" lepiej zostawić dla trwałego stanu.

Błąd nr 2: traktowanie static jako zamiennika magazynu.
static nie przeżywa restartu i nie synchronizuje różnych instancji. To nadal pamięć procesu, tylko szerzej dostępna.

Błąd nr 3: mieszanie obliczeń z faktami.
Policzenie sumy zamówienia jest przydatne. Ale dopóki samo zamówienie nie istnieje jako stan systemu, suma jest jedynie wartością pośrednią, a nie zapisanym wynikiem.

Błąd nr 4: myślenie, że problem zaczyna się dopiero przy JPA.
Nie. Problem zaczyna się wcześniej — w momencie, gdy aplikacja powinna utrzymywać dane między żądaniami, a ty nadal myślisz kategoriami lokalnych obiektów.

1
Zadanie
Spring Data JPA, poziom 1, lekcja 0
Niedostępne
Katalog tylko w pamięci
Katalog tylko w pamięci
1
Zadanie
Spring Data JPA, poziom 1, lekcja 0
Niedostępne
Pamięć kontra plik
Pamięć kontra plik
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION