Skip to main content
Advertisement

10.3 @Transactional Declarative Transactions and MyBatis Integration

Except for statistics or single-retrieve functions, almost all business collection/modification logic in modern backend applications internally involves multiple steps of DB state changes (INSERT, UPDATE, etc.).

1. Necessity of Transactions

For example, in an "Order Payment" service:

  1. Deduct member balance (UPDATE)
  2. Decrease inventory (UPDATE)
  3. Record order receipt (INSERT)

Suppose steps 1 and 2 succeed, but a server failure occurs at step 3. The already deducted balance and inventory become orphaned data. Therefore, these three operations must succeed and be applied all at once (COMMIT), or if even one fails, be reset to the state before step 1 (ROLLBACK).

2. Spring + MyBatis Transaction Mechanism

When using mybatis-spring-boot-starter in Spring Boot, MyBatis SqlSession operations are automatically managed by Spring's DataSourceTransactionManager.

  • How it works: When a method annotated with @Transactional is called, a Spring AOP proxy intercepts the request, acquires a DB connection, and starts a transaction.
  • MyBatis Integration: MyBatis Mapper methods share the same connection managed by Spring during execution. Once the operation is complete, the proxy performs the commit or rollback.

3. Practical Example (MyBatis Integration)

@Service
@RequiredArgsConstructor
public class ShopService {

private final UserMapper userMapper; // MyBatis Mapper
private final ProductMapper productMapper; // MyBatis Mapper

@Transactional(rollbackFor = Exception.class)
public void buyItem(Long userId, Long productId, int quantity) {
// 1. Deduct user balance (MyBatis UPDATE)
userMapper.updateBalance(userId, -10000);

// 2. Decrease product stock (MyBatis UPDATE)
int updatedRows = productMapper.updateStock(productId, -quantity);

// Business exception handling (triggers rollback)
if (updatedRows == 0) {
throw new RuntimeException("Insufficient stock to process the order.");
}
}
}

4. Transaction Propagation and Isolation Levels

You can fine-tune the scope of transactions depending on the situation.

AttributeDescriptionNote
REQUIREDJoins an existing parent transaction or creates a new one if none exists.Default (Most common)
REQUIRES_NEWAlways creates a new transaction regardless of a parent transaction.Used for independent tasks like logging.
READ_COMMITTEDReads only data that has been committed by other transactions.Default isolation level.
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
public void saveLog() { ... }

5. Default Rollback Rules and Precautions

Spring's @Transactional is powerful because it operates based on AOP (Aspect-Oriented Programming) proxies, but there are important rollback rules to note.

  • By default, Spring only performs a rollback when an Unchecked Exception(RuntimeException and its subclasses) or an Error occurs.
  • If a Checked Exception(e.g., IOException, SQLException) enforced by the Java compiler occurs, it does not rollback and commits instead.

Solution: Use the rollbackFor = Exception.class option to ensure rollbacks for all exceptions, or design business exceptions as runtime exceptions.

6. timeout and readOnly (Production Use)

Use timeout so long-running transactions do not hold DB connections indefinitely, and readOnly for read-only methods so the DB/driver can apply read optimizations.

AttributeDescriptionExample
timeoutMax transaction wait time in seconds; throws exception when exceeded.timeout = 5
readOnlyRead-only transaction; write attempts throw. Can enable DB/driver optimizations.readOnly = true
noRollbackForDo not roll back for specified exception(s). Use when treating as “failure” but keeping DB state.noRollbackFor = BusinessException.class
@Transactional(readOnly = true, timeout = 10)
public List<OrderDto> findOrdersByUser(Long userId) {
return orderRepository.findByUserId(userId);
}

@Transactional(rollbackFor = Exception.class, timeout = 5, noRollbackFor = DuplicateOrderException.class)
public void createOrder(OrderRequest dto) {
// Duplicate order: throw but do not roll back
}
Advertisement