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.
GO TO FULL VERSION