CodeGym /Kursy /Hibernate deep-dive /Odczyt śladu SQL: Product i PurchaseOrder

Odczyt śladu SQL: Product i PurchaseOrder

Hibernate deep-dive
Poziom 3, Lekcja 3
Dostępny

1. Ślad SQL jako „czarna skrzynka" Hibernate

Log SQL jest już włączony i służy jako narzędzie robocze. Przez niego można zobaczyć trzy sytuacje, które od zewnątrz łatwo pomylić: oczekiwany UPDATE u managed-encji, przypadkowy UPDATE w scenariuszu odczytu i ładowanie read-only, przy którym UPDATE w ogóle nie idzie. Jeśli kod wygląda niewinnie, a w logu jest UPDATE — wierzymy logowi.

Z punktu widzenia tego wykładu log SQL jest potrzebny, żeby odpowiedzieć na cztery bardzo konkretne pytania:

1) Czy w ogóle był UPDATE?
2) Po której tabeli był?
3) Dlaczego był — co dokładnie się zmieniło względem snapshotu?
4) Gdzie „urodził się" w kodzie: to oczekiwany zapis czy accidental update?

Krótka prosta myśl, do której wrócimy jak do mantry: log SQL to prawda. Kod może wyglądać niewinnie, ale jeśli w logu jest UPDATE, to znaczy, że ktoś zmienił managed-encję. I tym „kimś" — niespodziewanie — jesteś ty.

flowchart TD
    A["Początek metody @Transactional"] --> B["find() / query -> encja staje się managed"]
    B --> C["Hibernate trzyma snapshot"]
    C --> D["Kod zmienia pola encji"]
    D --> E["Hibernate widzi różnice od snapshotu"]
    E --> F["Na końcu transakcji wysyłany jest SQL UPDATE"]

2. Co dokładnie patrzeć w sql-trace

Potrzebujemy dwóch typów komunikatów z logu.

Typ pierwszy — same zapytania SQL: select ..., update ..., insert .... Zwykle daje je logger org.hibernate.SQL.

Typ drugi — wartości parametrów, które Hibernate podstawia do prepared statement. W logach to wygląda mniej więcej tak:

logging:
  level:
    org.hibernate.SQL: DEBUG          # Wypisujemy same zapytania SQL (select/update/insert)
    org.hibernate.orm.jdbc.bind: TRACE # Wypisujemy wartości podstawiane za '?'

spring:
  jpa:
    properties:
      hibernate:
        format_sql: true              # Sprawia, że SQL w logu jest czytelniejszy

3. Scenariusz nr 1: zmieniamy Product — oczekiwany UPDATE

Zacznijmy od najprostszego wzorca: załadowaliśmy managed Product, zmieniliśmy pole, w logu czekamy SELECT, a potem oczekiwany UPDATE po product.

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class CatalogService {

    private final ProductRepository productRepository;

    public CatalogService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Transactional
    public void renameProduct(Long id, String newName) {
        // 1) Ładujemy encję => staje się managed w persistence context
        Product product = productRepository.findById(id).orElseThrow();

        // 2) Zmieniamy pole => dirty checking wykryje różnicę od snapshotu
        product.setName(newName);

        // 3) save() nie jest potrzebne: UPDATE pójdzie przy flush/commit transakcji
    }
}

W logu zobaczysz mniej więcej:

-- 1) Ładowanie encji (SELECT), żeby ją uczynić managed
select p1_0.id, p1_0.name, p1_0.sku from product p1_0 where p1_0.id=?
-- binding parameter [1] as [BIGINT] - [1]

-- 2) Aktualizacja (UPDATE) przy zakończeniu transakcji/flush
update product set name=? where id=?
-- binding parameter [1] as [VARCHAR] - [Monitor 27]
-- binding parameter [2] as [BIGINT] - [1]

4. Scenariusz nr 2: zmieniamy PurchaseOrder — oczekiwany UPDATE

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final PurchaseOrderRepository orderRepository;

    public OrderService(PurchaseOrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Transactional
    public void confirmOrder(Long orderId) {
        // 1) Ładujemy zamówienie => managed
        PurchaseOrder order = orderRepository.findById(orderId).orElseThrow();

        // 2) Zmieniamy status => stanie się dirty
        order.setStatus(OrderStatus.CONFIRMED);

        // 3) save() nie jest potrzebne: UPDATE pójdzie przy zakończeniu transakcji
    }
}
-- 1) SELECT dla załadowania zamówienia
select o1_0.id, o1_0.status from purchase_order o1_0 where o1_0.id=?

-- 2) UPDATE z powodu zmiany status
update purchase_order set status=? where id=?
-- binding parameter [1] as [VARCHAR] - [CONFIRMED]

5. Accidental update: odczyt zamienia się w zapis

Teraz patrzymy na bardziej nieprzyjemny wariant: metoda wygląda jak odczyt, a w logu i tak pojawia się UPDATE.

import org.springframework.transaction.annotation.Transactional;

@Transactional
public String loadProductTitleDangerously(Long id) {
    // Ładujemy managed-encję
    Product product = productRepository.findById(id).orElseThrow();

    // UWAGA: to zapis do managed-obiektu, nawet jeśli "wydaje się kosmetyką"
    product.setName(product.getName().trim());

    // Zwracanie stringa nie sprawia automatycznie, że metoda jest read-only
    return product.getSku() + " " + product.getName();
}

Bezpieczna wersja:

import org.springframework.transaction.annotation.Transactional;

@Transactional
public String loadProductTitleSafely(Long id) {
    Product product = productRepository.findById(id).orElseThrow();

    // Normalizujemy do zmiennej lokalnej, nie ruszamy managed-encji
    String normalizedName = product.getName().trim();

    return product.getSku() + " " + normalizedName;
}

6. Read-only: zmieniamy w pamięci, SQL milczy

Dla pełni obrazu — odwrotna sygnatura: obiekt w pamięci ruszyliśmy, a UPDATE w logu tak i nie pojawił się. To właśnie ścieżka read-only.

import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Transactional
public List<Product> loadCatalogReadOnly(EntityManager entityManager) {
    TypedQuery<Product> q = entityManager.createQuery(
            "select p from Product p", Product.class
    );

    return q.setHint("org.hibernate.readOnly", true)
            .getResultList();
}

Jeśli po tym wywołasz setter, w logu nie powinien pojawić się UPDATE product, nawet jeśli zmieniłeś pole w pamięci.

7. Algorytm: kod → SQL

Scenariusz w kodzie Co robimy z managed-encją Czego oczekujemy w SQL Na co patrzeć w logu
findById + setName("...") Jawna zmiana pola UPDATE product ... where id=? Czy jest update product, czy zgadza się id
findById + setStatus(...) Jawna zmiana pola UPDATE purchase_order ... where id=? Czy jest update purchase_order
„Odczyt", ale wewnątrz setX(x.trim()) Niezamierzony zapis UPDATE pojawia się „nagle" Która tabela się zaktualizowała, jaki parametr jest bindowany
read-only query + zmiana w pamięci Zmieniamy obiekt, ale Hibernate „nie śledzi" UPDATE nie powinien się pojawić Brak update ... dla tej tabeli

8. Typowe błędy przy czytaniu logu SQL

Błąd nr 1: czytanie logu SQL bez powiązania z kodem i transakcją.
Bez granicy @Transactional nie rozumiesz, które SQL dotyczą konkretnego scenariusza. Lek: wstawiaj „znaczniki" (println/logger) i zawsze zaczynaj analizę od pytania „którą metodę teraz wykonuję?".

Błąd nr 2: szukanie UPDATE wyłącznie tam, gdzie jest save().
To jeden z najbardziej trwałych fałszywych nawyków. W Hibernate UPDATE pojawia się nie dlatego, że wywołałeś save(), lecz dlatego, że managed-encja stała się dirty.

Błąd nr 3: uważanie każdego „szerokiego" UPDATE za dowód buga.
Czasem zmieniłeś jedno pole, a w SQL aktualizuje się kilka kolumn. Na wczesnym etapie kursu to normalne — traktuj to jako techniczną cechę generowania SQL. Najpierw upewnij się, że UPDATE jest oczekiwany i poszedł po właściwej tabeli.

Błąd nr 4: pomijanie bind-parametrów i analizowanie tylko „nagiego SQL".
update product set name=? where id=? bez wartości — jak paragon bez kwoty. Do diagnozowania accidental update prawie zawsze potrzebne są wartości parametrów.

Błąd nr 5: mylenie scenariusza read-only ze zwykłym managed-scenariuszem.
Jeśli załadowałeś encje przez zwykłe find()/findById() i oczekujesz, że „to był odczyt, więc aktualizacji nie będzie" — sam siebie oszukujesz. Odczyt — to nie twoja intencja w głowie, lecz konkretne zachowanie Hibernate. Jeśli scenariusz ma być chroniony od zapisu, użyj read-only query lub entity-level read-only i weryfikuj wynik przez log SQL.

1
Zadanie
Hibernate deep-dive, poziom 3, lekcja 3
Niedostępne
Read-only query dla jednego produktu
Read-only query dla jednego produktu
1
Zadanie
Hibernate deep-dive, poziom 3, lekcja 3
Niedostępne
Entity-level read-only dla `PurchaseOrder`
Entity-level read-only dla `PurchaseOrder`
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION