Cheat SheetsHibernate & JPATransactions

Transactions — Cheat Sheet

Hibernate & JPA · 5 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Transactions
Hibernate & JPA5 topicsQuick revision reference
1

Transactions in JPA

JPA transactions are demarcated by EntityTransaction or (in Spring) @Transactional; a transaction spans the persistence context lifecycle for a unit of work.

  • JPA requires an active transaction for writes; Spring @Transactional provides AOP-based begin/commit/rollback.
  • Entities are managed within a transaction and automatically flushed to DB before relevant queries (AUTO mode).
  • @Transactional(readOnly = true) skips dirty-checking snapshots — use it for all read-only service methods.
  • Only RuntimeException triggers rollback by default — always specify rollbackFor for checked exceptions on write methods.
  • Merging a detached entity issues a SELECT + UPDATE; fetch fresh from DB inside the transaction when possible.
  • Open Session in View pattern is an anti-pattern in production — it extends the persistence context into the view layer, causing N+1 and resource leaks.
Java — entity lifecycle within @Transactional
@Service
@Transactional  // class-level: all public methods participate in a tx
public class OrderService {

    @PersistenceContext
    private EntityManager em;

    // New entity: em.persist() moves it to managed state
    public Order createOrder(OrderRequest req) {
        Order order = new Order(req.getCustomerId(), req.getItems());
        em.persist(order);          // state: NEW → MANAGED
        return order;               // entity is still managed here
    }                               // tx commits → flush → INSERT → entity DETACHED

    // Fetching within tx: entity is managed (dirty-checking active)
    @Transactional
    public void updateStatus(Long id, String status) {
        Order order = em.find(Order.class, id); // state: MANAGED
        order.setStatus(status);                // dirty-checked, no explicit save
    }                               // tx commits → flush → UPDATE auto-issued

    // Detached entity — requires merge
    @Transactional
    public Order updateDetached(Order detachedOrder) {
        return em.merge(detachedOrder); // DETACHED → MANAGED (SELECT + UPDATE)
    }

    // Remove
    @Transactional
    public void cancel(Long id) {
        Order order = em.find(Order.class, id);
        em.remove(order);           // state: MANAGED → REMOVED
    }                               // tx commits → DELETE
}
2

@Transactional Behaviour

@Transactional creates or joins a transaction on method entry and commits (or rolls back on unchecked exception) on exit; proxying means self-invocation bypasses the advice.

  • @Transactional works via AOP proxy — self-invocation (calling a @Transactional method from the same class) completely bypasses transaction management.
  • Spring only rolls back on RuntimeException and Error by default; add rollbackFor = Exception.class for checked exceptions.
  • readOnly = true skips Hibernate dirty-checking and flush — always use it for read operations to improve performance.
  • One @Transactional method = one Hibernate Session = one persistence context = shared first-level cache for all queries in that method.
  • Dirty checking means you do not need to call save() on entities you modify inside a transaction — Hibernate detects and flushes changes automatically.
  • @Transactional on a private method has no effect — Spring proxies only intercept public method calls from external callers.
Java — @Transactional + JPA Session
@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepo;
    private final CustomerRepository customerRepo;

    @Transactional   // one Session, one transaction for this entire method
    public Order createOrder(Long customerId, OrderRequest req) {
        // Both queries share the same Session (first-level cache)
        Customer customer = customerRepo.findById(customerId).orElseThrow();
        Order order = new Order(customer, req);
        orderRepo.save(order);  // INSERT — queued in Session, not sent to DB yet

        customer.setOrderCount(customer.getOrderCount() + 1);
        // No explicit save() needed — dirty checking detects the change
        // customerRepo.save(customer)  ← NOT required, Hibernate handles this

        return order;
        // ON RETURN: flush() → SQL sent → transaction committed → Session closed
    }

    // Without @Transactional: each repository call opens+closes its own mini-transaction
    // Each call = separate Session → no dirty checking across calls, no shared cache
    public Order findOrder(Long id) {
        return orderRepo.findById(id).orElseThrow();
    }
}
3

Transaction Propagation

REQUIRED (default, join or create), REQUIRES_NEW (always new, suspends outer), NESTED, SUPPORTS, NOT_SUPPORTED, MANDATORY, and NEVER — choose based on desired transactional boundary.

  • REQUIRED (default): join existing or create new — both methods share the same transaction
  • REQUIRES_NEW: always creates a new transaction, suspending the outer — good for audit logs
  • NESTED: uses a JDBC savepoint — inner rollback does not affect the outer transaction
  • MANDATORY: throws if no active transaction exists — useful as a safety assertion
  • Self-invocation bypasses the AOP proxy — @Transactional on the called method is ignored
  • Fix self-invocation: inject the bean's proxy via constructor or extract to a separate bean
Spring — REQUIRED vs REQUIRES_NEW propagation
@Service
class OrderService {

    @Transactional                          // REQUIRED (default)
    public void placeOrder(Order order) {
        orderRepo.save(order);
        inventoryService.reserve(order);    // joins THIS transaction
        auditService.log("ORDER_PLACED");   // also joins — but we want it independent!
        // If inventoryService throws, audit log is also rolled back
    }
}

@Service
class AuditService {

    // REQUIRES_NEW: suspend caller's transaction, open a fresh one
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String event) {
        auditRepo.save(new AuditEntry(event, LocalDateTime.now()));
        // This commits independently — even if placeOrder() rolls back
    }
}

// Important caveat: REQUIRES_NEW does NOT work when called on the same bean
// (Spring AOP proxy is bypassed for self-invocation)
// Always inject the service as a dependency, not via this.auditService.log()
4

Optimistic Locking — @Version

@Version on an integer/timestamp field enables optimistic locking; Hibernate includes the version in UPDATE WHERE clauses and throws OptimisticLockException on concurrent modification.

  • @Version adds a version column Hibernate includes in every UPDATE/DELETE WHERE clause — if the version changed, 0 rows are affected and OptimisticLockException is thrown.
  • Optimistic locking maximises concurrency by not holding DB locks; it detects conflicts only at commit time.
  • OptimisticLockException means a lost update was prevented — never swallow it; return HTTP 409 Conflict or retry the operation.
  • Pessimistic locking (SELECT FOR UPDATE) holds a DB lock for the transaction duration — use when conflicts are frequent or retry is not acceptable.
  • Never set the @Version field manually in your code — Hibernate manages it exclusively.
  • @Version on Instant/Timestamp can have sub-millisecond granularity issues; prefer Integer or Long version counters.
Java — @Version Entity
@Entity
public class BankAccount {

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

    private String owner;
    private BigDecimal balance;

    @Version   // Hibernate manages this — do NOT set it manually
    private Integer version;   // starts at 0, incremented on every UPDATE
}

// What Hibernate generates for an UPDATE:
// UPDATE bank_account
//   SET balance = ?, version = 1        ← increment version
//  WHERE id = 42
//   AND version = 0;                    ← must match what we read!
//
// If another transaction already updated version to 1,
// this WHERE matches 0 rows → Hibernate throws OptimisticLockException
5

Pessimistic Locking

PESSIMISTIC_READ (shared lock) and PESSIMISTIC_WRITE (exclusive lock) via EntityManager.lock() or query lock hints prevent concurrent modification at the DB level.

  • PESSIMISTIC_WRITE → SELECT FOR UPDATE: exclusive lock, prevents all concurrent reads and writes
  • PESSIMISTIC_READ → SELECT FOR SHARE: shared lock, allows concurrent reads, prevents writes
  • Must call locking inside a @Transactional method — lock is held until commit/rollback
  • SKIP LOCKED (timeout=-2) skips already-locked rows — ideal for distributed job queues
  • NOWAIT (timeout=0) throws PessimisticLockException immediately if the row is locked
  • Pessimistic is right for high-contention scenarios; optimistic for low-contention with acceptable retry
JPA — PESSIMISTIC_WRITE (SELECT FOR UPDATE)
// EntityManager
@Transactional
public void reserve(Long inventoryId, int qty) {
    // SELECT * FROM inventory WHERE id=? FOR UPDATE
    Inventory inv = entityManager.find(Inventory.class, inventoryId,
                                       LockModeType.PESSIMISTIC_WRITE);
    if (inv.getAvailable() < qty)
        throw new InsufficientStockException();
    inv.setAvailable(inv.getAvailable() - qty);
    // Lock released on transaction commit
}

// Spring Data repository
public interface InventoryRepository extends JpaRepository<Inventory, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT i FROM Inventory i WHERE i.id = :id")
    Optional<Inventory> findByIdForUpdate(@Param("id") Long id);
}

// Usage (must be inside @Transactional)
@Transactional
public void reserve(Long id, int qty) {
    Inventory inv = inventoryRepo.findByIdForUpdate(id).orElseThrow();
    inv.setAvailable(inv.getAvailable() - qty);
}
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/hibernate