Hibernate i JPA - Pytania Rekrutacyjne dla Java Developer [2026]

Sławomir Plamowski 21 min czytania
backend hibernate java jpa orm pytania-rekrutacyjne spring-data

Hibernate i JPA to fundament dostępu do danych w aplikacjach Java enterprise. Na rozmowie rekrutacyjnej pytania o ORM są praktycznie pewne - od podstaw entity mapping, przez relacje i transakcje, po zaawansowane tematy jak optymalizacja i cache. Ten przewodnik zawiera 55+ pytań rekrutacyjnych z odpowiedziami, które pomogą Ci przygotować się do rozmowy.

Podstawy JPA i Hibernate

Czym jest JPA i czym różni się od Hibernate?

Odpowiedź w 30 sekund:

  • JPA (Java Persistence API) - specyfikacja (interfejsy, adnotacje)
  • Hibernate - implementacja JPA (konkretna biblioteka)
  • Możesz zamienić Hibernate na EclipseLink bez zmiany kodu JPA

Odpowiedź w 2 minuty:

JPA i Hibernate współpracują ze sobą - JPA definiuje standardowy interfejs, a Hibernate dostarcza konkretną implementację z dodatkowymi funkcjami:

JPA (specyfikacja)          Hibernate (implementacja)
─────────────────           ─────────────────────────
@Entity                  →  Hibernate ORM
@Table                   →  Session, SessionFactory
EntityManager            →  Criteria API
JPQL                     →  HQL (rozszerzenie JPQL)

JPA definiuje "co" - interfejsy EntityManager, EntityTransaction, adnotacje jak @Entity, @Table, @Column. Hibernate definiuje "jak" - implementuje te interfejsy, dodaje własne rozszerzenia (HQL, kryteria, cache drugiego poziomu).

Korzyść z JPA: możesz teoretycznie zamienić Hibernate na EclipseLink bez zmiany kodu (w praktyce zdarza się rzadko). W Spring Boot używasz Spring Data JPA, które pod spodem używa Hibernate.


Co to jest EntityManager i jaka jest jego rola?

Odpowiedź w 30 sekund:

EntityManager to główny interfejs JPA do operacji na encjach:

  • persist() - zapisuje nową encję
  • find() - pobiera po ID
  • merge() - aktualizuje detached encję
  • remove() - usuwa encję
  • createQuery() - tworzy zapytania JPQL

Odpowiedź w 2 minuty:

EntityManager oferuje podstawowe operacje CRUD oraz zaawansowane możliwości zarządzania encjami. Oto przykład typowego repozytorium:

@Repository
public class UserRepository {
    @PersistenceContext
    private EntityManager em;

    public void save(User user) {
        if (user.getId() == null) {
            em.persist(user);  // INSERT
        } else {
            em.merge(user);    // UPDATE
        }
    }

    public User findById(Long id) {
        return em.find(User.class, id);  // SELECT by PK
    }

    public List<User> findByName(String name) {
        return em.createQuery(
            "SELECT u FROM User u WHERE u.name = :name", User.class)
            .setParameter("name", name)
            .getResultList();
    }

    public void delete(User user) {
        em.remove(em.contains(user) ? user : em.merge(user));
    }
}

EntityManager zarządza Persistence Context - zbiorem managed entities. Encje w kontekście są śledzone (dirty checking), zmiany są synchronizowane z bazą przy flush/commit.

W Spring Data JPA rzadko używasz EntityManager bezpośrednio - JpaRepository robi to za Ciebie.


Jakie są stany encji w JPA?

Odpowiedź w 30 sekund:

  1. New/Transient - nowy obiekt, nie ma ID, nie jest w kontekście
  2. Managed - w persistence context, śledzony przez Hibernate
  3. Detached - był managed, ale kontekst został zamknięty
  4. Removed - oznaczony do usunięcia

Odpowiedź w 2 minuty:

Cykl życia encji przechodzi przez różne stany w zależności od operacji wykonywanych przez EntityManager. Poniższy przykład ilustruje przejścia między stanami:

User user = new User("John");  // NEW - nie ma ID

em.persist(user);               // MANAGED - ma ID, śledzony
user.setName("Jane");           // zmiana będzie zapisana automatycznie!

em.detach(user);                // DETACHED - nie jest już śledzony
user.setName("Bob");            // ta zmiana NIE zostanie zapisana

User merged = em.merge(user);   // merged jest MANAGED, user nadal DETACHED

em.remove(merged);              // REMOVED - zostanie usunięty przy flush
     persist()           detach()/close()
NEW ──────────→ MANAGED ────────────────→ DETACHED
                  ↑↓                         │
                  │ merge()                  │
                  ←──────────────────────────┘
                  │
                  │ remove()
                  ↓
               REMOVED

Ważne: merge() zwraca NOWY managed obiekt - oryginalny pozostaje detached!


Co to jest Dirty Checking i jak działa?

Odpowiedź w 30 sekund:

Dirty Checking to automatyczne wykrywanie zmian w managed entities. Hibernate porównuje aktualny stan z snapshotem z momentu pobrania. Przy flush/commit generuje UPDATE tylko dla zmienionych pól.

Odpowiedź w 2 minuty:

Dirty Checking eliminuje potrzebę ręcznego wywoływania metody save() dla zaktualizowanych encji. Hibernate automatycznie wykrywa zmiany i generuje odpowiednie zapytania UPDATE:

@Transactional
public void updateUserName(Long id, String newName) {
    User user = em.find(User.class, id);  // Hibernate zapisuje snapshot
    user.setName(newName);                 // Zmiana w pamięci
    // Nie ma save()! Hibernate sam wykryje zmianę i zrobi UPDATE
}

Jak to działa pod spodem:

1. find() → SELECT + snapshot: {id=1, name="John", email="j@x.com"}
2. setName("Jane") → current: {id=1, name="Jane", email="j@x.com"}
3. flush/commit → porównanie snapshot vs current
4. Różnica w "name" → UPDATE users SET name='Jane' WHERE id=1

Kiedy flush?

  • Przed zapytaniem JPQL (auto-flush)
  • Przy em.flush() explicite
  • Przy commit() transakcji
  • Przy zamknięciu sesji (w zależności od FlushMode)

Entity Mapping

Jakie są podstawowe adnotacje do mapowania encji?

Odpowiedź w 30 sekund:

@Entity                    // oznacza klasę jako encję
@Table(name = "users")    // nazwa tabeli (opcjonalna)
@Id                        // klucz główny
@GeneratedValue           // auto-generowanie ID
@Column(name = "...")     // mapowanie kolumny

Odpowiedź w 2 minuty:

Adnotacje JPA pozwalają precyzyjnie kontrolować mapowanie encji na struktury bazodanowe. Poniższy przykład pokazuje pełną konfigurację encji z różnymi typami adnotacji:

@Entity
@Table(name = "users", schema = "public",
       uniqueConstraints = @UniqueConstraint(columnNames = {"email"}))
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "user_name", length = 100, nullable = false)
    private String name;

    @Column(unique = true)
    private String email;

    @Column(precision = 10, scale = 2)
    private BigDecimal salary;

    @Enumerated(EnumType.STRING)  // zapisuje nazwę enuma, nie ordinal
    private Status status;

    @Temporal(TemporalType.TIMESTAMP)  // dla java.util.Date
    private Date createdAt;

    @Transient  // nie mapowane do bazy
    private String temporaryData;

    @Lob  // dla dużych danych (BLOB/CLOB)
    private byte[] avatar;
}

Strategie generowania ID:

  • IDENTITY - auto-increment bazy (MySQL, PostgreSQL)
  • SEQUENCE - sekwencja (Oracle, PostgreSQL) - najwydajniejsza
  • TABLE - osobna tabela z sekwencją (przenośna, wolna)
  • AUTO - provider wybiera strategię

Jak mapować relacje w JPA?

Odpowiedź w 30 sekund:

@OneToMany   // jeden do wielu
@ManyToOne   // wiele do jednego
@OneToOne    // jeden do jednego
@ManyToMany  // wiele do wielu

Odpowiedź w 2 minuty:

Relacje między encjami można mapować jako jednokierunkowe lub dwukierunkowe, z różnymi strategiami ładowania i kaskadowania operacji. Przykład dwukierunkowej relacji:

// Dwukierunkowa relacja OneToMany / ManyToOne
@Entity
public class Department {
    @Id
    private Long id;

    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
    private List<Employee> employees = new ArrayList<>();
}

@Entity
public class Employee {
    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)  // właściciel relacji
    @JoinColumn(name = "department_id")
    private Department department;
}

Kluczowe koncepty:

Atrybut Znaczenie
mappedBy Wskazuje właściciela relacji (kto ma FK)
cascade Propagacja operacji (PERSIST, MERGE, REMOVE, ALL)
fetch LAZY vs EAGER loading
orphanRemoval Usuń sieroty przy usunięciu z kolekcji
// ManyToMany z tabelą pośrednią
@Entity
public class Student {
    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();
}

Czym różni się właściciel relacji od strony inverse?

Odpowiedź w 30 sekund:

  • Właściciel - strona z @JoinColumn, ta która "ma" klucz obcy w bazie
  • Inverse - strona z mappedBy, nie zarządza relacją w bazie

Zmiany na stronie inverse są IGNOROWANE przy zapisie!

Odpowiedź w 2 minuty:

W relacji dwukierunkowej tylko właściciel (strona z @JoinColumn) kontroluje zapis do bazy danych. Modyfikacje po stronie inverse są ignorowane:

@Entity
public class Post {
    @OneToMany(mappedBy = "post")  // inverse - mappedBy
    private List<Comment> comments;

    // Ta metoda NIE zapisze relacji!
    public void addComment(Comment c) {
        comments.add(c);  // tylko w pamięci
    }
}

@Entity
public class Comment {
    @ManyToOne
    @JoinColumn(name = "post_id")  // właściciel - ma FK
    private Post post;
}

Poprawne zarządzanie relacją:

// Metoda pomocnicza w Post
public void addComment(Comment comment) {
    comments.add(comment);
    comment.setPost(this);  // KLUCZOWE - ustaw obie strony!
}

// Lub ustaw właściciela bezpośrednio
comment.setPost(post);
em.persist(comment);  // to zapisze relację

Zasada: Zawsze ustawiaj relację po stronie właściciela (tej z @JoinColumn).


Jak mapować dziedziczenie encji?

Odpowiedź w 30 sekund:

Trzy strategie:

  1. SINGLE_TABLE - jedna tabela, kolumna discriminator
  2. JOINED - tabela per klasa, joiny przy select
  3. TABLE_PER_CLASS - osobna tabela per konkretna klasa

Odpowiedź w 2 minuty:

Strategia SINGLE_TABLE jest najczęściej stosowana ze względu na wydajność - wszystkie klasy potomne są przechowywane w jednej tabeli z kolumną rozróżniającą:

// SINGLE_TABLE (domyślna, najwydajniejsza)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type")
public abstract class Payment {
    @Id
    private Long id;
    private BigDecimal amount;
}

@Entity
@DiscriminatorValue("CARD")
public class CardPayment extends Payment {
    private String cardNumber;  // NULL dla innych typów
}

@Entity
@DiscriminatorValue("CASH")
public class CashPayment extends Payment {
    private String currency;
}
Strategia Zalety Wady
SINGLE_TABLE Szybka, brak joinów NULL kolumny, brak NOT NULL
JOINED Znormalizowana, NOT NULL Wolniejsza (joiny)
TABLE_PER_CLASS Niezależne tabele Wolne zapytania polimorficzne

Rekomendacja: SINGLE_TABLE dla prostych hierarchii, JOINED dla złożonych z wieloma polami.


Lazy vs Eager Loading

Czym różni się Lazy od Eager loading?

Odpowiedź w 30 sekund:

  • Lazy - dane ładowane dopiero przy pierwszym dostępie
  • Eager - dane ładowane od razu z główną encją

Domyślnie: @OneToMany, @ManyToMany → LAZY, @ManyToOne, @OneToOne → EAGER

Odpowiedź w 2 minuty:

Strategia ładowania danych wpływa na wydajność i zachowanie aplikacji. Lazy loading pobiera dane na żądanie, a eager loading ładuje wszystko od razu:

@Entity
public class User {
    @OneToMany(fetch = FetchType.LAZY)  // domyślne
    private List<Order> orders;  // ładowane przy user.getOrders()

    @ManyToOne(fetch = FetchType.EAGER)  // domyślne
    private Department department;  // ładowane od razu z User
}

Problem z Lazy poza transakcją:

@Transactional
public User getUser(Long id) {
    return userRepository.findById(id).get();  // orders nie załadowane
}

// W kontrolerze (poza transakcją):
user.getOrders();  // LazyInitializationException!

Rozwiązania:

  1. JOIN FETCH w zapytaniu
  2. @EntityGraph
  3. Open Session in View (antywzorzec!)
  4. DTO z potrzebnymi danymi

Co to jest problem N+1 i jak go rozwiązać?

Odpowiedź w 30 sekund:

N+1 to sytuacja gdy Hibernate wykonuje:

  • 1 zapytanie po listę encji
  • N zapytań po powiązane dane (przy lazy loading każdej)

Rozwiązanie: JOIN FETCH, @EntityGraph, @BatchSize, DTO projection

Odpowiedź w 2 minuty:

Problem N+1 jest jednym z najczęstszych problemów wydajnościowych w aplikacjach używających ORM. Poniższy przykład pokazuje problem i cztery różne sposoby jego rozwiązania:

// Problem N+1
List<User> users = em.createQuery("SELECT u FROM User u", User.class)
    .getResultList();  // 1 zapytanie

for (User user : users) {
    user.getOrders().size();  // N zapytań! (1 per user)
}
// Razem: 1 + N zapytań

Rozwiązanie 1: JOIN FETCH

List<User> users = em.createQuery(
    "SELECT u FROM User u JOIN FETCH u.orders", User.class)
    .getResultList();  // 1 zapytanie z JOIN

Rozwiązanie 2: @EntityGraph

@EntityGraph(attributePaths = {"orders"})
List<User> findAll();

// Lub named graph
@NamedEntityGraph(name = "User.withOrders",
    attributeNodes = @NamedAttributeNode("orders"))
@Entity
public class User { ... }

Rozwiązanie 3: @BatchSize

@OneToMany
@BatchSize(size = 25)  // ładuje orders dla 25 userów na raz
private List<Order> orders;
// Zamiast N zapytań → ceil(N/25) zapytań

Rozwiązanie 4: DTO Projection

@Query("SELECT new com.example.UserDTO(u.id, u.name, COUNT(o)) " +
       "FROM User u LEFT JOIN u.orders o GROUP BY u.id, u.name")
List<UserDTO> findUsersWithOrderCount();

Kiedy używać Lazy a kiedy Eager?

Odpowiedź w 30 sekund:

Lazy (zalecane domyślnie):

  • Kolekcje (@OneToMany, @ManyToMany)
  • Dane rzadko potrzebne
  • Duże obiekty (LOB)

Eager (wyjątkowo):

  • Małe, zawsze potrzebne obiekty
  • @ManyToOne gdy parent zawsze potrzebny

Odpowiedź w 2 minuty:

Wybór strategii ładowania powinien być świadomy i dostosowany do konkretnego przypadku użycia. Domyślnie warto używać Lazy loading i explicite określać co załadować w poszczególnych operacjach:

@Entity
public class Order {
    // LAZY - klient nie zawsze potrzebny przy listowaniu zamówień
    @ManyToOne(fetch = FetchType.LAZY)
    private Customer customer;

    // LAZY - szczegóły mogą być duże
    @OneToMany(fetch = FetchType.LAZY)
    private List<OrderItem> items;

    // EAGER może mieć sens - status zawsze mały i potrzebny
    @ManyToOne(fetch = FetchType.EAGER)
    private OrderStatus status;  // enum lub mała encja lookup
}

Best practice:

  1. Wszystko LAZY domyślnie
  2. Używaj JOIN FETCH / @EntityGraph dla konkretnych use case'ów
  3. Różne grafy dla różnych operacji:
interface UserRepository extends JpaRepository<User, Long> {
    // Dla listowania - bez orders
    List<User> findAll();

    // Dla szczegółów - z orders
    @EntityGraph(attributePaths = {"orders"})
    Optional<User> findWithOrdersById(Long id);

    // Dla raportu - z orders i items
    @EntityGraph(attributePaths = {"orders", "orders.items"})
    Optional<User> findWithFullOrdersById(Long id);
}

Transakcje i @Transactional

Jak działa @Transactional w Spring?

Odpowiedź w 30 sekund:

Spring tworzy proxy wokół klasy/metody z @Transactional. Proxy:

  1. Otwiera transakcję przed metodą
  2. Wykonuje metodę
  3. Commituje przy sukcesie lub rollback przy wyjątku

Odpowiedź w 2 minuty:

Spring używa mechanizmu proxy do zarządzania transakcjami. Gdy wywołujesz metodę oznaczoną @Transactional, Spring automatycznie otwiera transakcję, a następnie commituje lub rollbackuje w zależności od wyniku:

@Service
public class UserService {

    @Transactional
    public void createUser(User user) {
        userRepository.save(user);
        // commit automatyczny po zakończeniu metody
    }

    @Transactional
    public void failingMethod() {
        userRepository.save(new User("John"));
        throw new RuntimeException();  // rollback!
    }
}

Pod spodem:

// Spring generuje coś takiego:
public class UserService$$Proxy extends UserService {
    public void createUser(User user) {
        TransactionStatus tx = txManager.getTransaction(definition);
        try {
            super.createUser(user);
            txManager.commit(tx);
        } catch (RuntimeException e) {
            txManager.rollback(tx);
            throw e;
        }
    }
}

Kluczowe atrybuty:

  • propagation - jak zachować się gdy już jest transakcja
  • isolation - poziom izolacji
  • readOnly - optymalizacja dla odczytu
  • rollbackFor - dla jakich wyjątków rollback
  • timeout - max czas transakcji

Jakie są poziomy propagacji transakcji?

Odpowiedź w 30 sekund:

Propagation Zachowanie
REQUIRED Użyj istniejącej lub utwórz nową (domyślna)
REQUIRES_NEW Zawsze nowa, zawiesza bieżącą
NESTED Zagnieżdżona z savepoint
SUPPORTS Użyj jeśli jest, inaczej bez transakcji
NOT_SUPPORTED Bez transakcji, zawiesza bieżącą
MANDATORY Wymaga istniejącej, wyjątek jeśli brak
NEVER Wyjątek jeśli jest transakcja

Odpowiedź w 2 minuty:

Poziomy propagacji określają jak metody transakcyjne współpracują ze sobą. Najczęściej używane to REQUIRED (domyślny) i REQUIRES_NEW (osobna transakcja):

@Service
public class OrderService {

    @Transactional  // REQUIRED domyślnie
    public void processOrder(Order order) {
        orderRepository.save(order);
        notificationService.sendConfirmation(order);  // ta sama tx
    }
}

@Service
public class NotificationService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendConfirmation(Order order) {
        // Nowa transakcja - nawet jeśli order się zrollbackuje,
        // notyfikacja zostanie wysłana
        notificationRepository.save(new Notification(order));
    }
}

Typowe użycie REQUIRES_NEW:

  • Logowanie/audyt (zapisz nawet przy błędzie głównej operacji)
  • Zewnętrzne wywołania (nie blokuj głównej transakcji)
  • Retry logic (każda próba w osobnej transakcji)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAuditEvent(String event) {
    // Zapisze się nawet gdy główna transakcja się zrollbackuje
    auditRepository.save(new AuditEvent(event));
}

Jakie są pułapki @Transactional?

Odpowiedź w 30 sekund:

  1. Self-invocation - wywołanie @Transactional z tej samej klasy nie działa
  2. Checked exceptions - domyślnie nie powodują rollback
  3. Private methods - @Transactional nie działa
  4. Proxy - tylko zewnętrzne wywołania przechodzą przez proxy

Odpowiedź w 2 minuty:

Mechanizm proxy w Spring powoduje kilka nietypowych zachowań, które często zaskakują początkujących programistów. Oto najważniejsze pułapki:

Pułapka 1: Self-invocation

@Service
public class UserService {

    public void methodA() {
        methodB();  // NIE przejdzie przez proxy!
    }

    @Transactional
    public void methodB() {
        // Transakcja NIE zostanie rozpoczęta
    }
}

// Rozwiązanie: wstrzyknij siebie lub wydziel do osobnego serwisu
@Autowired
private UserService self;

public void methodA() {
    self.methodB();  // teraz przejdzie przez proxy
}

Pułapka 2: Checked exceptions

@Transactional
public void process() throws IOException {
    repository.save(entity);
    throw new IOException();  // NIE spowoduje rollback!
}

// Rozwiązanie
@Transactional(rollbackFor = IOException.class)
@Transactional(rollbackFor = Exception.class)  // wszystkie

Pułapka 3: Private methods

@Transactional  // NIE DZIAŁA!
private void doSomething() { }

Pułapka 4: Final class/method

// Spring nie może utworzyć proxy dla final
public final class UserService { }  // @Transactional nie zadziała

Co to jest @Transactional(readOnly = true)?

Odpowiedź w 30 sekund:

readOnly = true to wskazówka dla Hibernate i bazy danych:

  • Hibernate wyłącza dirty checking (szybciej)
  • Baza może użyć replica/read-only connection
  • Nie można modyfikować encji (lub zostanie zignorowane)

Odpowiedź w 2 minuty:

Użycie readOnly = true przynosi korzyści wydajnościowe i jasno komunikuje intencję operacji tylko do odczytu. Hibernate wyłącza wtedy mechanizm dirty checking:

@Service
public class ReportService {

    @Transactional(readOnly = true)
    public List<User> generateReport() {
        List<User> users = userRepository.findAll();
        // Dirty checking wyłączony - szybsze
        // Zmiany w users NIE zostaną zapisane
        return users;
    }

    @Transactional(readOnly = true)
    public UserDTO getUser(Long id) {
        User user = userRepository.findById(id).get();
        user.setName("Modified");  // OSTRZEŻENIE: zmiana zignorowana!
        return new UserDTO(user);
    }
}

Korzyści:

  1. Performance - brak snapshot dla dirty checking
  2. Routing - Spring może kierować na read replica
  3. Dokumentacja - jasno określa intencję metody
// Konfiguracja z read replica
@Configuration
public class DataSourceConfig {
    @Bean
    @Primary
    public DataSource routingDataSource() {
        return new ReadWriteRoutingDataSource(
            masterDataSource(), replicaDataSource()
        );
    }
}

Cache w Hibernate

Jakie są poziomy cache w Hibernate?

Odpowiedź w 30 sekund:

  1. First Level Cache - per Session/EntityManager, automatyczny
  2. Second Level Cache - per SessionFactory, współdzielony, opcjonalny
  3. Query Cache - cache wyników zapytań, opcjonalny

Odpowiedź w 2 minuty:

Hibernate wykorzystuje wielopoziomową architekturę cache dla optymalizacji dostępu do danych. Poniższy diagram pokazuje jak te poziomy współpracują:

┌─────────────────────────────────────────────────┐
│                 APPLICATION                      │
├─────────────────────────────────────────────────┤
│  Session 1          Session 2         Session 3 │
│  ┌─────────┐       ┌─────────┐       ┌─────────┐│
│  │ L1 Cache│       │ L1 Cache│       │ L1 Cache││
│  └────┬────┘       └────┬────┘       └────┬────┘│
│       │                 │                 │      │
│       └────────────────┬┴─────────────────┘      │
│                        ↓                         │
│              ┌─────────────────┐                 │
│              │  L2 Cache       │ (opcjonalny)    │
│              │  (SessionFactory)│                 │
│              └────────┬────────┘                 │
│                       ↓                          │
│              ┌─────────────────┐                 │
│              │   Query Cache   │ (opcjonalny)    │
│              └────────┬────────┘                 │
└───────────────────────┼─────────────────────────┘
                        ↓
                   DATABASE

L1 Cache (automatyczny):

@Transactional
public void example() {
    User u1 = em.find(User.class, 1L);  // SELECT
    User u2 = em.find(User.class, 1L);  // z L1 cache, brak SQL
    assert u1 == u2;  // ta sama instancja!
}

L2 Cache (wymaga konfiguracji):

@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User { }

Jak skonfigurować Second Level Cache?

Odpowiedź w 30 sekund:

  1. Dodaj provider (Ehcache, Caffeine, Redis)
  2. Włącz w konfiguracji
  3. Oznacz encje jako @Cacheable

Odpowiedź w 2 minuty:

Konfiguracja Second Level Cache wymaga dodania providera cache (np. Ehcache) oraz oznaczenia encji które mają być cachowane. Poniżej pełna konfiguracja:

<!-- pom.xml -->
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-jcache</artifactId>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
# application.yml
spring:
  jpa:
    properties:
      hibernate:
        cache:
          use_second_level_cache: true
          region.factory_class: jcache
        javax.cache:
          provider: org.ehcache.jsr107.EhcacheCachingProvider
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Country {
    @Id
    private Long id;
    private String name;
    // Rzadko się zmienia - idealny kandydat do cache
}

Cache strategies:

Strategy Opis Użycie
READ_ONLY Tylko odczyt Dane statyczne
READ_WRITE Odczyt/zapis z lockami Ogólne użycie
NONSTRICT_READ_WRITE Bez locków Eventual consistency OK
TRANSACTIONAL Full ACID Wymagana spójność

Kiedy używać Query Cache?

Odpowiedź w 30 sekund:

Query Cache cachuje wyniki zapytań (listę ID, nie encji). Użyj gdy:

  • Zapytanie jest często wykonywane
  • Dane rzadko się zmieniają
  • L2 Cache jest włączony (dla encji)

Odpowiedź w 2 minuty:

Query Cache wymaga włączenia w konfiguracji oraz explicite oznaczenia zapytań które mają być cachowane. Ważne: cachuje tylko listę ID, nie same encje:

spring:
  jpa:
    properties:
      hibernate:
        cache:
          use_query_cache: true
@Repository
public interface CountryRepository extends JpaRepository<Country, Long> {

    @QueryHints(@QueryHint(name = HINT_CACHEABLE, value = "true"))
    List<Country> findByContinent(String continent);
}

// Lub z EntityManager
List<Country> countries = em.createQuery(
        "SELECT c FROM Country c WHERE c.continent = :cont", Country.class)
    .setParameter("cont", "Europe")
    .setHint("org.hibernate.cacheable", true)
    .getResultList();

Jak działa:

Query: "SELECT c FROM Country c WHERE continent = 'Europe'"
Cache key: query + parametry
Cache value: [1, 5, 7, 12]  // lista ID

Przy kolejnym wywołaniu:
1. Sprawdź Query Cache → znaleziono [1, 5, 7, 12]
2. Dla każdego ID sprawdź L2 Cache → Country entities
3. Brak SQL!

Uwaga: Query Cache jest inwalidowany gdy JAKAKOLWIEK encja danego typu się zmieni. Dobre tylko dla danych read-mostly.


JPQL i Criteria API

Czym różni się JPQL od SQL?

Odpowiedź w 30 sekund:

  • JPQL - operuje na encjach i ich polach (obiektowy)
  • SQL - operuje na tabelach i kolumnach (relacyjny)

JPQL jest przenośny między bazami, SQL nie.

Odpowiedź w 2 minuty:

JPQL operuje na modelu obiektowym encji, podczas gdy SQL działa bezpośrednio na strukturze relacyjnej bazy danych. Oto porównanie tych samych zapytań:

// JPQL - używa nazw klas i pól
@Query("SELECT u FROM User u WHERE u.department.name = :deptName")
List<User> findByDepartmentName(String deptName);

// SQL - używa nazw tabel i kolumn
@Query(value = "SELECT * FROM users u " +
               "JOIN departments d ON u.department_id = d.id " +
               "WHERE d.name = :deptName",
       nativeQuery = true)
List<User> findByDepartmentNameNative(String deptName);

Kluczowe różnice:

Aspekt JPQL SQL
Operuje na Encjach Tabelach
Nazewnictwo camelCase (pola) snake_case (kolumny)
Relacje Automatyczne przez . Manualne JOIN
Przenośność Tak Nie
Funkcje Ograniczone Pełne DB-specific
// JPQL - automatyczny join przez relację
"SELECT u FROM User u WHERE u.department.manager.name = :name"

// SQL - trzeba ręcznie łączyć tabele
"SELECT u.* FROM users u
 JOIN departments d ON u.department_id = d.id
 JOIN users m ON d.manager_id = m.id
 WHERE m.name = :name"

Kiedy używać Criteria API zamiast JPQL?

Odpowiedź w 30 sekund:

Criteria API - dla dynamicznych zapytań z opcjonalnymi filtrami JPQL - dla statycznych zapytań (czytelniejsze)

Odpowiedź w 2 minuty:

Criteria API świeci się przy budowaniu zapytań z dynamicznymi, opcjonalnymi filtrami. Dla statycznych zapytań JPQL jest prostsze i bardziej czytelne:

// JPQL - statyczne zapytanie
@Query("SELECT u FROM User u WHERE u.status = :status AND u.role = :role")
List<User> findByStatusAndRole(Status status, Role role);

// Criteria API - dynamiczne filtry
public List<User> search(String name, Status status, Role role) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<User> cq = cb.createQuery(User.class);
    Root<User> user = cq.from(User.class);

    List<Predicate> predicates = new ArrayList<>();

    if (name != null) {
        predicates.add(cb.like(user.get("name"), "%" + name + "%"));
    }
    if (status != null) {
        predicates.add(cb.equal(user.get("status"), status));
    }
    if (role != null) {
        predicates.add(cb.equal(user.get("role"), role));
    }

    cq.where(predicates.toArray(new Predicate[0]));

    return em.createQuery(cq).getResultList();
}

Spring Data Specifications (wrapper na Criteria):

public interface UserRepository extends JpaRepository<User, Long>,
                                        JpaSpecificationExecutor<User> {}

// Użycie
Specification<User> spec = Specification
    .where(hasName(name))
    .and(hasStatus(status))
    .and(hasRole(role));

userRepository.findAll(spec);

Co to jest DTO Projection i kiedy jej używać?

Odpowiedź w 30 sekund:

DTO Projection pobiera tylko potrzebne kolumny zamiast całej encji. Użyj gdy:

  • Potrzebujesz subset pól
  • Chcesz uniknąć N+1
  • Dane są read-only

Odpowiedź w 2 minuty:

DTO Projection pozwala pobrać tylko niezbędne dane z bazy, unikając problemów z lazy loading i poprawiając wydajność. Spring Data oferuje dwa style projekcji:

// Interface-based projection (Spring Data)
public interface UserSummary {
    Long getId();
    String getName();
    String getDepartmentName();
}

@Query("SELECT u.id as id, u.name as name, u.department.name as departmentName " +
       "FROM User u")
List<UserSummary> findAllSummaries();

// Class-based projection (JPQL constructor)
public record UserDTO(Long id, String name, int orderCount) {}

@Query("SELECT new com.example.UserDTO(u.id, u.name, SIZE(u.orders)) " +
       "FROM User u")
List<UserDTO> findAllWithOrderCount();

Korzyści DTO Projection:

  1. Wydajność - mniej danych z bazy
  2. Brak N+1 - wszystko w jednym zapytaniu
  3. Brak lazy loading issues - to nie są encje
  4. Immutable - bezpieczne dla wielowątkowości
// Porównanie
// Entity (pobiera wszystko + może mieć N+1)
List<User> users = userRepository.findAll();

// DTO (pobiera tylko potrzebne kolumny, 1 zapytanie)
List<UserSummary> summaries = userRepository.findAllSummaries();

Optymalizacja i Best Practices

Jak zoptymalizować wydajność Hibernate?

Odpowiedź w 30 sekund:

  1. Używaj LAZY loading + JOIN FETCH
  2. Unikaj N+1 (batching, EntityGraph)
  3. DTO Projection dla read-only
  4. Włącz cache dla statycznych danych
  5. Batch inserts/updates

Odpowiedź w 2 minuty:

Optymalizacja Hibernate wymaga zarówno odpowiedniej konfiguracji, jak i świadomego pisania kodu. Kluczowe techniki to batch processing, monitoring i odpowiednie strategie ładowania:

# Batch processing
spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 50
          order_inserts: true
          order_updates: true
// Batch insert
@Transactional
public void saveAll(List<User> users) {
    for (int i = 0; i < users.size(); i++) {
        em.persist(users.get(i));
        if (i % 50 == 0) {
            em.flush();
            em.clear();  // zwolnij pamięć
        }
    }
}

// StatelessSession dla bulk operations
StatelessSession session = sessionFactory.openStatelessSession();
Transaction tx = session.beginTransaction();
for (User user : users) {
    session.insert(user);  // brak dirty checking, szybciej
}
tx.commit();
session.close();

Monitoring:

# Logowanie SQL
spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        generate_statistics: true  # statystyki cache, queries

logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.stat: DEBUG

Jakie są najczęstsze błędy przy używaniu Hibernate?

Odpowiedź w 30 sekund:

  1. N+1 queries - brak JOIN FETCH
  2. LazyInitializationException - dostęp poza sesją
  3. Self-invocation - @Transactional nie działa
  4. Dirty checking overhead - niepotrzebne modyfikacje
  5. Cascade delete - przypadkowe usunięcie danych

Odpowiedź w 2 minuty:

Poniżej znajdują się najczęstsze błędy popełniane przez developerów przy pracy z Hibernate, wraz z ich poprawnymi wersjami:

// ❌ Błąd 1: Porównywanie encji przez ==
if (user1 == user2)  // może nie działać dla detached!
// ✅ Poprawnie
if (user1.getId().equals(user2.getId()))

// ❌ Błąd 2: Modyfikacja encji "przypadkowo"
@Transactional
public UserDTO getUser(Long id) {
    User user = repo.findById(id).get();
    user.setLastAccess(LocalDateTime.now());  // UPDATE przy commit!
    return new UserDTO(user);
}
// ✅ Użyj readOnly lub osobną metodę do update

// ❌ Błąd 3: Cascade w złą stronę
@ManyToOne(cascade = CascadeType.REMOVE)  // usunie parent przy usunięciu child!
private Department department;
// ✅ Cascade tylko @OneToMany lub explicite delete

// ❌ Błąd 4: equals/hashCode z ID
@Override
public boolean equals(Object o) {
    return id.equals(((User) o).id);  // NIE dla nowych encji (id=null)
}
// ✅ Użyj business key lub UUID

Zobacz też


Ten artykuł jest częścią serii przygotowującej do rozmów rekrutacyjnych na stanowisko Java Backend Developer. Sprawdź nasze fiszki z pytaniami rekrutacyjnymi: Java.

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.