Transactions — Cheat Sheet
Hibernate & JPA · 5 topics. Download the PDF or the Instagram carousel and share it.
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.
@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
}@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.
@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();
}
}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
@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()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.
@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 OptimisticLockExceptionPessimistic 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
// 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);
}