CodeGym /Kursy /Hibernate deep-dive /Pierwsze uruchomienie w trybie diagnostycznym

Pierwsze uruchomienie w trybie diagnostycznym

Hibernate deep-dive
Poziom 1, Lekcja 4
Dostępny

1. Mikroskop dla kursu 🔬

Dni przygotowawcze w kursach technicznych mają złą reputację. Często zasłużoną: pół dnia ucieka na instalację, ustawienia i walkę ze środowiskiem, a wartości edukacyjnej prawie brak. W Hibernate deep-dive staramy się tego unikać. Dlatego dzisiejszy cel to nie tylko „odpalić projekt", ale zbudować stend diagnostyczny, na którym każdy przyszły scenariusz ORM będzie widoczny od razu w kilku wymiarach: jako kod, migracje, SQL, bind values i statystyki Hibernate. To już zupełnie inny poziom korzyści.

Dlaczego to tak ważne? Bo bez obserwowalności deep-dive bardzo szybko zamienia się w zgadywanie. Widzisz dziwne zachowanie, zakładasz przyczynę, zmieniasz adnotację, znowu masz nadzieję na lepsze — i żyjesz w trybie szamaństwa. Normalny kurs inżynierski powinien uczyć odwrotności: najpierw zbudować środowisko, gdzie zachowanie jest widoczne, a dopiero potem wyciągać wnioski.

Tu pojawia się pierwszy mocny moment tego kursu. Nie kończymy poziomu słowami „no, masz teraz projekt". Kończymy go stendem, w którym to samo wywołanie jest naprawdę widoczne i jako operacja biznesowa, i jako SQL, i jako parametry, i jako liczniki ORM. To bardzo praktyczny punkt startowy.

2. PostgreSQL przez Docker Compose 🐘

Lokalna baza w naszym kursie jest podnoszona przez Docker Compose. To nie jest dekoracyjne komplikowanie i nie „żeby wyglądało jak dorosła praca". To najprostszy sposób na odtwarzalne środowisko. Jeśli jeden student pracuje z lokalnym PostgreSQL w systemie, drugi z kontenerem, trzeci z H2 „na razie dla wygody", a czwarty nie jest nawet pewien, do której bazy się podłączył, to dalej nie dyskutujemy o Hibernate, lecz o obcych środowiskach.

Minimalny docker-compose.yml dla kursu może wyglądać tak:

services:
  postgres: # główny serwis z PostgreSQL
    image: postgres:18 # ustalamy wersję, żeby środowisko było odtwarzalne
    environment:
      POSTGRES_DB: commerce # nazwa bazy, która zostanie utworzona przy starcie kontenera
      POSTGRES_USER: commerce # użytkownik bazy
      POSTGRES_PASSWORD: commerce # hasło użytkownika (dla kursu — prosta kombinacja)
    ports:
      - "5432:5432" # mapowanie portu: na zewnątrz localhost:5432 -> wewnątrz kontenera :5432
    volumes:
      - commerce_pgdata:/var/lib/postgresql/data # przechowujemy dane poza kontenerem (persist)

volumes:
  commerce_pgdata: # nazwany volume dla danych PostgreSQL

Tu niemal wszystko jest przejrzyste. Kontener tworzy bazę commerce, użytkownika commerce i hasło commerce. Port jest mapowany na zewnątrz na standardowy 5432, żeby aplikacja Boot mogła się podłączać do localhost:5432. Volume jest potrzebny, żeby dane nie znikały przy każdym odtworzeniu kontenera. To wygodne w zwykłej lokalnej pracy i dobrze łączy się z ideą stend'u edukacyjnego.

Uruchomienie — standardowe:

docker compose up -d # podnieść serwisy w tle (detached)
docker compose ps    # sprawdzić, że kontener naprawdę jest uruchomiony i w statusie Up

Jeśli chcesz upewnić się, że kontener rzeczywiście żyje, a nie tylko istnieje w wyobraźni, możesz obejrzeć logi:

docker compose logs -f postgres # obserwować logi konkretnego serwisu postgres w trybie follow

Dobry lokalny baseline jest nudny. Nie ma w nim niespodziewanych portów, nie ma „specjalnego ręcznego setup", nie ma osobnej instrukcji na każdy laptop. I to jest dokładnie to, czego potrzebujemy. Im środowisko nudniejsze i stabilniejsze, tym ciekawsze jest obserwowane zachowanie ORM.

3. Podstawowa konfiguracja i profile: local, sql-trace, stats

Kolejny krok to podzielenie trybów uruchomienia tak, żeby środowisko nie zamieniło się w jeden gigantyczny plik YAML z wiecznym szumem. W projekcie są trzy główne profile: local, sql-trace i stats. local odpowiada za zwykłe lokalne uruchomienie i połączenie z PostgreSQL. sql-trace — za widoczność SQL i bind values. stats — za włączenie Hibernate statistics. To bardzo proste podział, ale od razu czyni stend zarządzalnym. 📌

Podstawowe reguły kursu wygodnie trzymać w application.yml:

spring:
  application:
    name: commerce-persistence-lab # nazwa aplikacji (dla logów/metryk/identyfikacji)

  jpa:
    open-in-view: false # nie trzymamy persistence context otwartego "do widoku"
    hibernate:
      ddl-auto: none # schemą NIE generuje Hibernate, tylko Flyway

  flyway:
    enabled: true # migracje włączone jako podstawowy mechanizm zarządzania schematem

Tu ważne są trzy linijki. open-in-view: false od razu ustala, że praca z bazą powinna żyć wewnątrz zrozumiałych granic backendowych, a nie wyciekać „gdzieś dalej". ddl-auto: none oznacza, że Hibernate nie zarządza schematem samodzielnie. Schemat żyje tylko przez Flyway. I flyway.enabled: true podkreśla, że migracje to nie opcja dodatkowa, lecz część podstawowego trybu projektu.

Profil local zazwyczaj przechowuje połączenie z bazą:

spring:
  config:
    activate:
      on-profile: local # ten blok stosuje się tylko przy aktywnym profilu local

  datasource:
    url: jdbc:postgresql://localhost:5432/commerce # łączymy się z postgres z docker compose
    username: commerce # powinno zgadzać się z POSTGRES_USER
    password: commerce # powinno zgadzać się z POSTGRES_PASSWORD

Profil sql-trace włącza widoczność SQL i parametrów:

spring:
  config:
    activate:
      on-profile: sql-trace # włączamy ten blok tylko gdy chcemy widzieć SQL/parametry

logging:
  level:
    org.hibernate.SQL: debug # wypisujemy SQL generowany przez Hibernate
    org.hibernate.orm.jdbc.bind: trace # wypisujemy bind values (co dokładnie podstawiło się w '?')

A profil stats włącza zbieranie statystyk:

spring:
  config:
    activate:
      on-profile: stats # włączamy statystyki tylko gdy potrzebne (nie szumimy zawsze)

  jpa:
    properties:
      hibernate.generate_statistics: true # Hibernate zaczyna zbierać Statistics (liczniki)

Takie rozwiązanie jest bardzo przydatne metodycznie. Możesz uruchamiać aplikację po prostu w local, gdy potrzebujesz cichego trybu. I możesz dodawać sql-trace,stats, gdy zaczynasz analizować zachowanie. Szum włącza się świadomie, a nie wisi w tle zawsze. To dojrzały nawyk, który świetnie przenosi się na realną pracę programistyczną.

4. Flyway: schemat powinien być widoczny

W kursie deep-dive o Hibernate schemat bazy to nie techniczna cień klas Java. To pełnoprawna część systemu. Jeśli nie kontrolujesz, jakie tabele, ograniczenia i indeksy rzeczywiście istnieją w PostgreSQL, to generated SQL jest trudny do interpretacji. Właśnie dlatego projekt zasadniczo żyje na Flyway, a nie na ddl-auto=update.

Niech startowa migracja wygląda bardzo prosto:

-- V1__baseline_catalog.sql
create table product (
    id bigint primary key, -- jawny PK (ważny dla ORM i czytelności modelu)
    sku varchar(64) not null unique, -- identyfikator biznesowy, NOT NULL + UNIQUE
    name varchar(255) not null -- nazwa produktu, też NOT NULL dla prostego baseline
);

-- dane seed: żeby już od pierwszego uruchomienia było co czytać z bazy
insert into product(id, sku, name)
values (1, 'SKU-0001', 'Coffee mug');

To już wystarczy, żeby zobaczyć, jak aplikacja stosuje schemat i dostaje dane seed. Po starcie możesz sprawdzić, że Flyway rzeczywiście zadziałał, nie tylko po logach aplikacji Boot, ale bezpośrednio w bazie. To dobry gest anty-magiczny. Na przykład:

docker compose exec postgres psql -U commerce -d commerce \ # wejść do kontenera i wykonać polecenie psql
  -c "select installed_rank, version, description, success from flyway_schema_history order by installed_rank;" # sprawdzić historię migracji

Jeśli tabela flyway_schema_history istnieje i pokazuje pomyślnie zastosowane migracje, to znaczy, że schemat żyje dokładnie tak, jak zaplanowano. A żeby sprawdzić obecność danych startowych, można zrobić jeszcze jedno uczciwe zapytanie:

docker compose exec postgres psql -U commerce -d commerce \
  -c "select count(*) from product;" # sprawdzamy, że dane seed zostały wstawione

Ten moment wydaje się „drobnym ustawieniem", ale w rzeczywistości buduje zaufanie do kursu. Nie masz nadziei, że schemat wstał. Widzisz, jak i czym wstał. A to znaczy, że generated SQL możesz czytać na solidnym fundamencie, a nie na domysłach.

5. Hibernate statistics i serwis smoke

Teraz trzeba dodać dwa małe kawałki techniczne, które dadzą nam realną obserwowalność. Pierwszy — dostęp do Statistics przez bieżący SessionFactory. Drugi — prosty serwis smoke, który wykonuje kilka operacji odczytu i wypisuje minimalny raport. To jak pierwsze uruchomienie laboratorium: nie robimy nic skomplikowanego, ale od razu widzimy, że diagnostyka naprawdę działa.

Konfiguracja dla Statistics może wyglądać tak:

@Configuration
class HibernateStatisticsConfig {

    @Bean
    Statistics hibernateStatistics(EntityManagerFactory emf) {
        // unwrap potrzebny, żeby dostać się do "natywnego" SessionFactory Hibernate z opakowania JPA
        return emf.unwrap(SessionFactory.class).getStatistics(); // pobieramy jedyny obiekt Statistics
    }
}

Dalej mały serwis do czyszczenia i wypisywania liczników:

@Service
class HibernateStatsPrinter {

    private final Statistics stats; // tu jest wstrzykiwane Statistics, jeśli aktywny profil stats

    HibernateStatsPrinter(Statistics stats) {
        this.stats = stats;
    }

    public void clear() {
        stats.clear(); // zerujemy liczniki, żeby liczyć tylko bieżący scenariusz
    }

    public void print() {
        // Prepared statements: ile wyrażeń JDBC przygotował Hibernate
        System.out.println("Prepared statements = " + stats.getPrepareStatementCount());
        // Entity loads: ile encji naprawdę się załadowało (przydatne do szukania N+1 i zbędnych odczytów)
        System.out.println("Entity loads = " + stats.getEntityLoadCount());
        // Flushes: ile razy nastąpił flush (ważne, żeby rozumieć, kiedy Hibernate pisze do bazy)
        System.out.println("Flushes = " + stats.getFlushCount());
    }
}

A teraz — sam scenariusz smoke. Ważne, żeby to była normalna operacja backendowa, a nie przypadkowy static helper. Niech więc żyje jako zwykły serwis:

@Service
class StartupSmokeService {

    private final ProductRepository products; // repozytorium Spring Data do dostępu do product
    private final HibernateStatsPrinter statsPrinter; // wypisywanie statystyk po scenariuszu

    StartupSmokeService(ProductRepository products, HibernateStatsPrinter statsPrinter) {
        this.products = products;
        this.statsPrinter = statsPrinter;
    }

    @Transactional(readOnly = true) // ważny moment: czytamy w transakcji (baseline dla JPA)
    public void run() {
        statsPrinter.clear(); // zaczynamy od "czystej tablicy"

        long count = products.count(); // szybkie zapytanie, żeby zobaczyć SQL i przygotowanie wyrażenia
        System.out.println("Products in DB = " + count);

        // pobieranie po PK: tu właśnie pojawi się parametr bind (id = ?)
        products.findById(1L).ifPresent(product ->
                System.out.println("Loaded product sku = " + product.getSku())
        );

        statsPrinter.print(); // wypisujemy liczniki Hibernate dokładnie dla tego scenariusza
    }
}

Żeby to wszystko naprawdę wykonało się przy starcie, wygodnie podpiąć CommandLineRunner:

@Configuration
class SmokeStartupConfig {

    @Bean
    CommandLineRunner smokeRunner(StartupSmokeService smokeService) {
        // CommandLineRunner uruchamia się zaraz po starcie kontekstu Spring Boot
        return args -> smokeService.run(); // wywołujemy nasz scenariusz smoke
    }
}

Tu prawie nie ma logiki biznesowej i to normalne. Cel nie w tym, żeby „zrobić feature". Cel w tym, żeby udowodnić: projekt naprawdę startuje, naprawdę stosuje migracje, naprawdę komunikuje się z PostgreSQL, naprawdę pisze SQL do logu i naprawdę oddaje statystyki dla tej operacji. To właśnie pierwsza robocza wygrana dzisiejszego dnia.

6. Co powinieneś zobaczyć w logach i konsoli 👀

Teraz uruchamiamy aplikację w pełnym trybie diagnostycznym:

./gradlew bootRun --args="--spring.profiles.active=local,sql-trace,stats" # aktywujemy profile: baza + SQL/bind + statystyki

Jeśli uruchamiasz z IDE, sens pozostaje ten sam: muszą być aktywne profile local, sql-trace, stats. Ważne nie tyle skąd uruchamiasz, ile żeby uruchomienie było odtwarzalne i zgodne z baseline kursu.

Po starcie powinieneś zobaczyć mniej więcej taką sekwencję zdarzeń. Najpierw podnosi się datasource. Potem Flyway informuje o zastosowanych migracjach. Następnie odpala się serwis smoke. I właśnie tu pojawiają się główne sygnały:

# Poniżej — przykład kluczowych linii (mogą się różnić formatowaniem, ale sens ten sam)
DEBUG org.hibernate.SQL - select count(p1_0.id) from product p1_0
DEBUG org.hibernate.SQL - select p1_0.id,p1_0.name,p1_0.sku from product p1_0 where p1_0.id=?
TRACE org.hibernate.orm.jdbc.bind - binding parameter (1:BIGINT) <- [1]
Products in DB = 1
Loaded product sku = SKU-0001
Prepared statements = 2
Entity loads = 1
Flushes = 0

Co tu ważne zobaczyć wzrokiem? Po pierwsze, SQL rzeczywiście jest widoczny. Po drugie, bind value jest widoczna. Czyli ? w zapytaniu nie pozostaje abstrakcją — widzisz konkretny parametr 1. Po trzecie, statystyki rzeczywiście liczą pracę Hibernate, a nie istnieją „gdzieś w teorii". Po czwarte, serwis smoke potwierdza, że operacja naprawdę doszła do bazy i odczytała dane.

Oto mocny moment dzisiejszego dnia: jeden maleńki use case jest już widoczny od razu w kilku wymiarach. Masz kod Java. Masz migrację, która stworzyła tabelę i seed. Masz SQL. Masz bind parameters. Masz liczniki Hibernate. To jest dokładnie ten rodzaj praktycznego wyniku, który jest potrzebny kursowi deep-dive. Nie „aplikacja jakoś wystartowała", ale „mam działający stend diagnostyczny". 🔬

7. Lista kontrolna działającego stend'u diagnostycznego

Żeby ten wynik można było przenosić dalej przez kurs, warto ustalić krótką listę kontrolną. Jeśli kiedyś po drodze przestaniesz widzieć jeden z tych sygnałów, to już powód, by nie analizować Hibernate dalej, lecz najpierw przywrócić sam stend.

Co sprawdzić Znak, że wszystko w porządku
PostgreSQL jest podniesiony docker compose ps pokazuje żywy kontener postgres
Aplikacja podłączyła się do właściwej bazy datasource wskazuje na jdbc:postgresql://localhost:5432/commerce
Schemat stworzony przez Flyway istnieje tabela flyway_schema_history, migracje oznaczone jako pomyślne
ORM nie zmienia schematu sam ddl-auto=none
Kontekst nie jest sztucznie rozciągnięty open-in-view=false
SQL jest widoczny aktywny profil sql-trace, logi org.hibernate.SQL się pojawiają
Parametry są widoczne aktywny logger org.hibernate.orm.jdbc.bind
Statistics liczy profil stats aktywny, liczniki są wypisywane
Scenariusz smoke naprawdę trafił do bazy jest count() / findById() i zrozumiały wydruk w konsoli

To właśnie jest pierwszy namacalny wynik dnia. Można go używać nie tylko dziś. Przyda się za każdym razem, gdy w kursie trzeba będzie zrozumieć, czy rzeczywiście obserwujesz zachowanie ORM, czy tylko patrzysz na piękny kod Java bez informacji zwrotnej z bazy.

8. Typowe błędy i most do następnego kroku 🚧

Błąd nr 1: zatrzymanie się na fakcie startu aplikacji.
Aplikacja Boot może wystartować, a użytecznego sygnału z data-layer i tak nie dostaniesz. Bez scenariusza smoke i logów wciąż pracujesz prawie w ciemno.

Błąd nr 2: włączenie tylko SQL, ale nie bind values.
Tekst zapytania sam w sobie jest przydatny, ale bardzo często połowa sensu siedzi właśnie w realnych parametrach. Bez nich diagnostyka szybko staje się niepełna.

Błąd nr 3: pozostawienie ddl-auto=update dla wygody.
To natychmiast podważa zaufanie do schematu. A kurs, który uczy łączenia Javy z SQL, nie może sobie pozwolić na płynną strukturę bazy.

Błąd nr 4: trzymanie wszystkich logów włączonych zawsze i przestanie ich czytać.
Profile są właśnie po to, żeby obserwowalność włączała się świadomie i nie zamieniała się w biały szum.

1
Zadanie
Hibernate deep-dive, poziom 1, lekcja 4
Niedostępne
Uruchomienie diagnostyczne z profilami local i sql-trace
Uruchomienie diagnostyczne z profilami local i sql-trace
1
Zadanie
Hibernate deep-dive, poziom 1, lekcja 4
Niedostępne
Liczniki Hibernate Statistics dla jednego scenariusza startowego
Liczniki Hibernate Statistics dla jednego scenariusza startowego
1
Ankieta/quiz
Głęboki Hibernate, poziom 1, lekcja 4
Niedostępny
Głęboki Hibernate
Rozumienie ORM i SQL
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION