Skip to content

Transactions & Locking

This content is for v1.0. Switch to the latest version for up-to-date documentation.

Use transactions and row-level locks when writes must be consistent across multiple queries.

$entityManager->transactional(function (EntityManager $em) use ($entity) {
$em->persist($entity);
$em->flush();
return $entity;
});

The wrapper commits when the callback returns and rolls back when the callback throws.

  • beginTransaction() starts a transaction.
  • commit() flushes and commits.
  • rollback() rolls back pending database work.

Manual control is useful when a command needs to demonstrate intermediate failure states or lock behavior.

Use lock() on the query builder for SELECT ... FOR UPDATE:

$stock = $entityManager
->createQueryBuilder(StockLock::class)
->where('product_id', $productId)
->lock()
->getSingleResult();

An inventory-decrement flow locks stock rows before decrementing:

$stock = $entityManager
->createQueryBuilder(StockLock::class)
->where('product_id', $productId)
->lock()
->getSingleResult();
  • Calling lock() outside an active transaction raises a transaction-required error.
  • Acquire multiple locks in a deterministic order to reduce deadlock risk.
  • Keep transaction callbacks small; long-running work should happen before or after the transaction where possible.

Pessimistic locking (SELECT ... FOR UPDATE) blocks other writers for the duration of a transaction. Contrast with Optimistic Locking, which never locks and instead detects conflicts at write time.