Hibernate i JPA - Pytania Rekrutacyjne dla Java Developer [2026]
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:
- New/Transient - nowy obiekt, nie ma ID, nie jest w kontekście
- Managed - w persistence context, śledzony przez Hibernate
- Detached - był managed, ale kontekst został zamknięty
- 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:
- SINGLE_TABLE - jedna tabela, kolumna discriminator
- JOINED - tabela per klasa, joiny przy select
- 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:
- JOIN FETCH w zapytaniu
@EntityGraph- Open Session in View (antywzorzec!)
- 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:
- Wszystko LAZY domyślnie
- Używaj JOIN FETCH / @EntityGraph dla konkretnych use case'ów
- 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:
- Otwiera transakcję przed metodą
- Wykonuje metodę
- 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:
- Self-invocation - wywołanie @Transactional z tej samej klasy nie działa
- Checked exceptions - domyślnie nie powodują rollback
- Private methods - @Transactional nie działa
- 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:
- Performance - brak snapshot dla dirty checking
- Routing - Spring może kierować na read replica
- 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:
- First Level Cache - per Session/EntityManager, automatyczny
- Second Level Cache - per SessionFactory, współdzielony, opcjonalny
- 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:
- Dodaj provider (Ehcache, Caffeine, Redis)
- Włącz w konfiguracji
- 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:
- Wydajność - mniej danych z bazy
- Brak N+1 - wszystko w jednym zapytaniu
- Brak lazy loading issues - to nie są encje
- 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:
- Używaj LAZY loading + JOIN FETCH
- Unikaj N+1 (batching, EntityGraph)
- DTO Projection dla read-only
- Włącz cache dla statycznych danych
- 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:
- N+1 queries - brak JOIN FETCH
- LazyInitializationException - dostęp poza sesją
- Self-invocation - @Transactional nie działa
- Dirty checking overhead - niepotrzebne modyfikacje
- 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ż
- Kompletny Przewodnik - Rozmowa Java Backend Developer - pełny przewodnik przygotowania do rozmowy
- Spring Boot - Pytania Rekrutacyjne - 56 pytań Spring Framework
- SQL Pytania Rekrutacyjne - zapytania SQL i optymalizacja
- Java - Pytania Rekrutacyjne - 150 pytań Java Core
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.
