Persistence Context
entity lifecycle, L1/L2 cache, dirty checking, flush stratégiák, optimistic locking
Persistence Context
A Persistence Context a JPA központi mechanizmusa: az EntityManager identity map-je, amely az entity-k állapotát követi, dirty checking-et végez, és többszintű cache rendszert kezel.
1. Definíció
- Mi ez? — A Persistence Context (PC) az EntityManager-hez kötött memóriabeli tárolóhely, amely a managed entity-ket tartja nyilván. Olyan mint egy identity map: minden entity-nek egy referenciáját tárolja, és biztosítja a repeatable read garanciát.
- Miért létezik? — A PC csökkenti a DB-lekérdezések számát (L1 cache), automatikusan detektálja a változásokat (dirty checking), és konzisztens entity referenciákat ad egy tranzakción belül.
- Hol helyezkedik el? — A PC az EntityManager-en belül él. Spring-ben tipikusan egy tranzakcióhoz kötődik (
@Transactional). A tranzakció végén a PC flush-öl és bezárul.
EntityManager
└── Persistence Context (L1 Cache)
├── User#1 → managed instance
├── User#2 → managed instance
└── Order#5 → managed instance
2. Alapfogalmak
Entity állapotok (lifecycle)
| Állapot | Leírás | PC ismeri? | Van ID? |
|---|---|---|---|
| New (Transient) | Új objektum, new User() |
❌ | ❌ (vagy van, de PC nem tudja) |
| Managed | persist() vagy find() után |
✅ | ✅ |
| Detached | Session/tranzakció bezárása után | ❌ | ✅ |
| Removed | remove() után, flush-kor DELETE |
✅ (törlésre jelölve) | ✅ |
New ──persist()──→ Managed ──flush()──→ DB INSERT
│
remove() ──→ Removed ──flush()──→ DB DELETE
│
session close ──→ Detached
↑
merge() ←──────── Detached
Állapotátmenetek részletesen
// New → Managed
User user = new User("Alice", "alice@mail.com"); // New
entityManager.persist(user); // Managed (ID generálva)
// Managed → DB szinkronizálás
user.setName("Bob"); // dirty checking figyeli
// flush-kor: UPDATE users SET name='Bob' WHERE id=1
// Managed → Detached
entityManager.detach(user); // vagy: tranzakció vége
user.setName("Charlie"); // NEM detektálva! Nem managed.
// Detached → Managed
User mergedUser = entityManager.merge(user); // Managed copy
// Figyelem: user ≠ mergedUser (a merge másolatot ad vissza)
// Managed → Removed
entityManager.remove(user); // Removed
// flush-kor: DELETE FROM users WHERE id=1
First-Level Cache (L1)
Az L1 cache a Persistence Context része, mindig aktív és nem kikapcsolható.
User u1 = em.find(User.class, 1L); // SQL SELECT → DB hit
User u2 = em.find(User.class, 1L); // Nincs SQL! → cache hit
assert u1 == u2; // true — ugyanaz a referencia
// Repeatable read garancia:
// Ugyanazt az entity-t ugyanabban a PC-ben mindig
// ugyanaz a Java objektum reprezentálja.
Az L1 cache a tranzakció végén törlődik. Mérete nincs korlátozva — nagy batch műveleteknél manuális clear() szükséges.
Dirty checking
A dirty checking automatikus változásdetektálás a flush pillanatában:
- Entity betöltésekor a Hibernate deep copy snapshot-ot készít
- Flush-kor összehasonlítja az aktuális mezőértékeket a snapshot-tal
- Ha van eltérés → UPDATE SQL generálás
@DynamicUpdatenélkül az összes oszlopot UPDATE-eli@DynamicUpdate-tel csak a módosult oszlopokat
@Transactional
public void updateUserName(Long id, String newName) {
User user = userRepository.findById(id).orElseThrow();
user.setName(newName); // dirty
// Nem kell save() — a dirty checking automatikusan UPDATE-el flush-kor
}
Flush stratégiák
| FlushMode | Mikor flush-öl | Használat |
|---|---|---|
| AUTO (alapértelmezett) | Query előtt + tranzakció commit-kor | ✅ Legtöbb esetben |
| COMMIT | Csak tranzakció commit-kor | Teljesítmény-optimalizálás |
| MANUAL | Csak explicit em.flush() hívásakor |
Speciális batch műveletek |
// AUTO: query előtt automatikusan flush-öl a konzisztencia érdekében
user.setName("Updated");
List<User> users = em.createQuery("SELECT u FROM User u", User.class)
.getResultList();
// ↑ A query előtt flush történik, hogy az "Updated" név látszódjon az eredményben
3. Gyakorlati használat
Mikor fontos a PC tudatos kezelése
- Batch INSERT/UPDATE (>1000 elem): rendszeres
flush()+clear()szükséges, különben OutOfMemoryError - Read-only query-k:
@Transactional(readOnly = true)→ Hibernate kihagyja a dirty checking snapshot-ot - Detached entity kezelés: DTO pattern vagy
merge()használata - Hosszú beszélgetés (conversation): Extended Persistence Context vagy explicit merge
Batch feldolgozás minta
@Transactional
public void batchInsert(List<UserDto> dtos) {
int batchSize = 50;
for (int i = 0; i < dtos.size(); i++) {
User user = new User(dtos.get(i).getName(), dtos.get(i).getEmail());
entityManager.persist(user);
if (i > 0 && i % batchSize == 0) {
entityManager.flush(); // SQL-ek kiírása
entityManager.clear(); // L1 cache ürítése → memória felszabadítás
}
}
}
Read-only optimalizálás
@Service
public class ReportService {
@Transactional(readOnly = true)
public List<UserSummary> getReport() {
// readOnly = true előnyei:
// 1. Nincs dirty checking snapshot → kevesebb memória
// 2. Hibernate FLUSH_MODE_MANUAL-ra vált → nincs auto-flush
// 3. DB szinten read-only tranzakció hint
return userRepository.findAllProjectedBy();
}
}
Detached entity és a merge() minta
// Controller → Service → DB flow:
@RestController
public class UserController {
@PutMapping("/users/{id}")
public UserDto updateUser(@PathVariable Long id, @RequestBody UserDto dto) {
return userService.update(id, dto);
}
}
@Service
public class UserService {
@Transactional
public UserDto update(Long id, UserDto dto) {
User user = userRepository.findById(id).orElseThrow();
// ✅ A managed entity-t módosítjuk → dirty checking
user.setName(dto.getName());
user.setEmail(dto.getEmail());
// Nem kell save() — a tranzakció végén auto-flush
return UserDto.from(user);
}
}
4. Kód példák
Second-Level Cache (L2)
Az L2 cache alkalmazás-szintű, a SessionFactory-hoz kötött. Tranzakciók között is megmarad.
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Country {
@Id
private String code;
private String name;
}
# application.yml
spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
region.factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
javax:
cache:
provider: org.ehcache.jsr107.EhcacheCachingProvider
Cache szintek összefoglalás
| Szint | Scope | Alapértelmezett | Tartalom |
|---|---|---|---|
| L1 (PC) | EntityManager / tranzakció | Mindig aktív | Entity referenciák |
| L2 | SessionFactory / alkalmazás | Manuális konfiguráció | Entity snapshot-ok |
| Query Cache | SessionFactory | Manuális konfiguráció | Query eredmény ID-k |
// L2 cache működés:
// Tranzakció A:
Country hu = em.find(Country.class, "HU"); // L1 miss → L2 miss → DB SELECT
// Tranzakció A vége → L1 törlődik, de L2-be mentődik
// Tranzakció B:
Country hu = em.find(Country.class, "HU"); // L1 miss → L2 HIT! → nincs DB
Cache concurrency stratégiák
| Stratégia | Konzisztencia | Teljesítmény | Mikor |
|---|---|---|---|
| READ_ONLY | ✅ Erős | ✅ Legjobb | Immutable adatok (Country, Currency) |
| NONSTRICT_READ_WRITE | ⚠️ Eventual | ✅ Jó | Ritkán változó, nem kritikus |
| READ_WRITE | ✅ Erős | ⚠️ Közepes | Gyakran olvasott, időnként írt |
| TRANSACTIONAL | ✅ ACID | ❌ Lassú | JTA tranzakció szükséges |
Optimistic Locking (@Version)
@Entity
public class Product {
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Version
private Integer version;
private String name;
private BigDecimal price;
}
Működés:
- Entity betöltésekor a version érték is betöltődik (pl. version=3)
- UPDATE SQL:
UPDATE product SET name=?, price=?, version=4 WHERE id=? AND version=3 - Ha a WHERE 0 sort érint →
OptimisticLockException - A kliens retry-t vagy merge-et végezhet
@Service
public class ProductService {
@Transactional
public void updatePrice(Long id, BigDecimal newPrice) {
Product product = productRepository.findById(id).orElseThrow();
product.setPrice(newPrice);
// flush-kor: UPDATE ... WHERE version=? → ha ütközés: OptimisticLockException
}
}
// Retry pattern kivétel kezeléshez:
@Retryable(value = OptimisticLockException.class, maxAttempts = 3)
@Transactional
public void updatePriceWithRetry(Long id, BigDecimal newPrice) {
Product product = productRepository.findById(id).orElseThrow();
product.setPrice(newPrice);
}
Pessimistic Locking
// SELECT ... FOR UPDATE
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Optional<Product> findByIdWithLock(@Param("id") Long id);
| Locking | Mechanizmus | Mikor |
|---|---|---|
| Optimistic | @Version + application szintű |
Alacsony ütközés (legtöbb CRUD) |
| Pessimistic | SELECT FOR UPDATE DB lock |
Magas ütközés (inventory, booking) |
5. Trade-offok
| Szempont | Előny | Hátrány |
|---|---|---|
| L1 Cache | Automatikus, nincs konfiguráció, repeatable read | Memória-probléma nagy batch-nél |
| Dirty checking | Nem kell explicit save/update | Rejtett UPDATE-ek, teljesítmény overhead |
| L2 Cache | Tranzakciók közötti cache, DB terhelés csökkentés | Cache invalidáció bonyolult, konzisztencia kockázat |
| Optimistic locking | Nincs DB lock, szabad olvasás | Ütközésnél retry szükséges |
| Pessimistic locking | Garantált exkluzív hozzáférés | Deadlock kockázat, lassú |
| readOnly=true | Nincs snapshot, kevesebb memória | Write-nál figyelmen kívül hagyja a változásokat |
6. Gyakori hibák
❌ save() felesleges hívása managed entity-n
// ROSSZ — felesleges DB roundtrip
@Transactional
public void updateUser(Long id, String name) {
User user = userRepository.findById(id).orElseThrow();
user.setName(name);
userRepository.save(user); // ← felesleges! A dirty checking megoldja
}
// JÓ — a dirty checking automatikusan UPDATE-el
@Transactional
public void updateUser(Long id, String name) {
User user = userRepository.findById(id).orElseThrow();
user.setName(name);
// flush-kor automatikusan UPDATE
}
❌ Detached entity módosítása flush nélkül
// ROSSZ — a módosítás elvész
public void updateUser(Long id, String name) { // nincs @Transactional!
User user = userRepository.findById(id).orElseThrow();
user.setName(name);
// Session/PC már bezárt → nincs flush → a módosítás elvész
}
❌ Batch feldolgozás clear() nélkül
// ROSSZ — OutOfMemoryError nagy adatsetnél
@Transactional
public void importAll(List<Data> items) {
for (Data d : items) {
entityManager.persist(new Record(d));
// Az L1 cache folyamatosan nő → OOM
}
}
❌ L2 cache gyakran változó adaton
Ha az entity gyakran módosul, az L2 cache invalidáció költségesebb, mint az adatbázis-lekérdezés. L2 cache-t ritkán változó, gyakran olvasott adatokra használj (pl. Country, Category, Configuration).
❌ @Version figyelmen kívül hagyása UPDATE DTO-nál
// ROSSZ — a frontend nem küldi vissza a version-t → elvész az optimistic lock
@PutMapping("/products/{id}")
public void update(@RequestBody ProductDto dto) {
Product p = productRepository.findById(dto.getId()).orElseThrow();
p.setName(dto.getName());
// A version mező nincs szinkronizálva!
}
// JÓ — a DTO tartalmazza a version-t
public class ProductDto {
private Long id;
private String name;
private Integer version; // ← a frontend visszaküldi
}
❌ merge() vs persist() félreértése
// persist(): New → Managed (az eredeti objektumot kezeli)
User user = new User("Alice");
em.persist(user); // user most managed
// merge(): Detached → Managed COPY (új managed példányt ad vissza!)
User detached = /* ... */;
User managed = em.merge(detached); // managed ≠ detached!
detached.setName("X"); // NEM hat a DB-re — detached maradt
managed.setName("Y"); // Ez lesz az UPDATE
7. Mélyebb összefüggések
Hibernate internal: snapshot array
A Hibernate a dirty checking-hez Object[] tömböt használ snapshot-ként, nem deep clone-t. Minden managed entity-hez két tömb tartozik: az aktuális állapot és a betöltéskori állapot. A flush-kor ezeket hasonlítja össze mező-szinten.
Ez azt jelenti:
- Primitív típusoknál és String-eknél egyszerű
equals()összehasonlítás - Mutable objektumoknál (Date, Collection) mélyebb összehasonlítás
- Nagy entity-knél (30+ mező) a dirty checking overhead érezhető
@DynamicUpdate optimalizálás
@Entity
@DynamicUpdate // Csak a módosult oszlopokat UPDATE-eli
public class Product {
// 20+ mező...
}
// @DynamicUpdate NÉLKÜL (alapértelmezett):
// UPDATE product SET name=?, price=?, description=?, ... WHERE id=?
// Minden oszlop szerepel, még ha csak a name változott
// @DynamicUpdate-TEL:
// UPDATE product SET name=? WHERE id=?
// Csak a módosult oszlopok
Mikor érdemes: sok oszlopú entity, ahol jellemzően 1-2 mező változik. Hátránya: a Hibernate nem tudja a prepared statement-et cache-elni (minden UPDATE más SQL).
Extended Persistence Context
Alapesetben a PC a tranzakció végén bezárul. Az Extended PC a tranzakción túl is él — tipikusan @Stateful EJB-ben vagy Spring @Scope("session") bean-ben.
// Spring-ben ritkán használt — helyette DTO pattern + merge()
@PersistenceContext(type = PersistenceContextType.EXTENDED)
private EntityManager em;
⚠️ Extended PC problémái: memória-szivárgás, stale data, bonyolult lifecycle kezelés. Legtöbb Spring alkalmazásban kerülendő.
Query Cache részletesen
A Query Cache az entity ID-ket cache-eli, nem az entity-ket magukat:
Query: "SELECT u FROM User u WHERE u.status = 'ACTIVE'"
Query Cache: [1, 5, 12, 34] ← entity ID-k
A query cache hit után → L2 cache-ből tölti be az entity-ket ID alapján
Mikor érdemes:
- Fix paraméterű, gyakran futó query-k
- Csak ha az érintett entity-k L2 cache-ben is vannak
hibernate.cache.use_query_cache=trueszükséges
Flush és auto-flush viselkedés
Az AUTO flush mode biztosítja, hogy query előtt a pending változások kiíródnak:
user.setName("Updated"); // dirty, de még nincs SQL
// Ez a JPQL query flush-t triggerel (mert a User tábla érintett):
List<User> result = em.createQuery("SELECT u FROM User u").getResultList();
// ↑ Előtte: flush() → UPDATE users SET name='Updated'
// ↑ Utána: SELECT * FROM users → az "Updated" név már látszik
// Ez a query NEM triggerel flush-t (más tábla):
List<Order> orders = em.createQuery("SELECT o FROM Order o").getResultList();
// ↑ A User táblát nem érinti → nincs flush
8. Interjúkérdések
K: Mi a Persistence Context és hogyan működik az L1 cache? V: A PC az EntityManager identity map-je. Ugyanazt az entity-t egyetlen tranzakcióban mindig ugyanaz a Java referencia reprezentálja. Az L1 cache mindig aktív, a tranzakció végén törlődik.
K: Mikor kell explicit flush() és clear()? V: Batch feldolgozásnál (>1000 elem), hogy az L1 cache ne nőjön korlátlanul. Rendszeres flush() kiírja a pending SQL-eket, clear() felszabadítja a memóriát.
K: Mi a különbség a persist() és merge() között? V: A persist() New → Managed átmenetet csinál, az eredeti objektumot kezeli. A merge() Detached → Managed COPY-t csinál — az eredeti objektum detached marad, egy új managed példányt ad vissza.
K: Hogyan működik a dirty checking? V: Entity betöltésekor snapshot készül. Flush-kor a Hibernate összehasonlítja az aktuális állapotot a snapshot-tal. Ha eltérés van, UPDATE SQL generálódik. @DynamicUpdate-tel csak a módosult oszlopok kerülnek bele.
K: Mi a @Version és hogyan kezeli a párhuzamos módosítást? V: A @Version optimistic locking mező. UPDATE SQL-ben WHERE feltételbe kerül. Ha két tranzakció módosítja, a második OptimisticLockException-t kap, mert a version nem egyezik.
K: Mi a különbség az L1 és L2 cache között? V: L1: EntityManager/tranzakció scope, mindig aktív, entity referenciákat tart. L2: SessionFactory scope, manuálisan konfigurált, entity snapshot-okat tart, tranzakciók között is megmarad.
K: Miért előnyös a @Transactional(readOnly = true)? V: A Hibernate kihagyja a dirty checking snapshot-ot → kevesebb memória, nincs auto-flush, a DB olvasásra optimalizálhat (read-only hint).
9. Szószedet
| Fogalom | Jelentés |
|---|---|
| Persistence Context | EntityManager identity map-je, entity referenciák |
| L1 Cache | Ugyanaz mint a PC — tranzakció scope, mindig aktív |
| L2 Cache | SessionFactory scope-ú, manuálisan konfigurált cache |
| Query Cache | Lekérdezés eredmény ID-k cache-elése |
| Managed | Entity állapot: a PC követi, dirty checking figyeli |
| Detached | Entity állapot: session bezárása után, van ID, nincs tracking |
| Dirty checking | Automatikus változásdetektálás flush pillanatában |
| Flush | Pending változások SQL-be írása |
| Snapshot | Entity betöltéskori állapot deep copy-ja |
| @Version | Optimistic locking verzió mező |
| @DynamicUpdate | Csak módosult oszlopokat UPDATE-eli |
| Extended PC | Tranzakción túl is élő Persistence Context |
10. Gyorsreferencia
ENTITY LIFECYCLE:
New → persist() → Managed
Managed → remove() → Removed → flush() → DELETE
Managed → close() → Detached
Detached → merge() → Managed (COPY!)
persist() ≠ merge() → persist az eredetit kezeli, merge másolatot ad
L1 CACHE (Persistence Context):
Mindig aktív, tranzakció scope
find() kétszer → 1 SQL
u1 == u2 → true (identity garantált)
Batch: flush() + clear() rendszeresen
DIRTY CHECKING:
Betöltéskor snapshot → flush-kor összehasonlítás
@DynamicUpdate → csak módosult oszlopok
readOnly=true → nincs snapshot → kevesebb memória
FLUSH MODE:
AUTO query előtt + commit-kor (default)
COMMIT csak commit-kor
MANUAL csak explicit em.flush()
L2 CACHE:
@Cacheable + @Cache(usage=...)
READ_ONLY immutable adat
READ_WRITE gyakran olvasott
SessionFactory scope, konfigurálni kell
LOCKING:
@Version optimistic (nincs DB lock)
@Lock(PESSIMISTIC) SELECT FOR UPDATE
Optimistic → alacsony ütközés (CRUD)
Pessimistic → magas ütközés (inventory)
TIPP-EK:
Managed entity → nem kell save()
Batch 1000+ → flush/clear ciklusban
readOnly=true → report query-khez
merge() másolatot ad, nem az eredetit!
🎮 Játékok
10 kérdés