Haladó Olvasási idő: ~13 perc

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:

  1. Entity betöltésekor a Hibernate deep copy snapshot-ot készít
  2. Flush-kor összehasonlítja az aktuális mezőértékeket a snapshot-tal
  3. Ha van eltérés → UPDATE SQL generálás
  4. @DynamicUpdate nélkül az összes oszlopot UPDATE-eli
  5. @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:

  1. Entity betöltésekor a version érték is betöltődik (pl. version=3)
  2. UPDATE SQL: UPDATE product SET name=?, price=?, version=4 WHERE id=? AND version=3
  3. Ha a WHERE 0 sort érint → OptimisticLockException
  4. 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=true szü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