or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-patterns.mdcore-operations.mddatabase-init.mddatasource.mdembedded-database.mdexception-translation.mdindex.mdkey-generation.mdlob-support.mdresult-parameter-handling.mdsimplified-operations.mdsqlrowset.mdtransactions.md
tile.json

transactions.mddocs/

Transaction Management

Spring JDBC integrates seamlessly with Spring's transaction management infrastructure, providing declarative and programmatic transaction support for JDBC operations.

Core Classes

DataSourceTransactionManager

PlatformTransactionManager implementation for single JDBC DataSource.

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean {
    public DataSourceTransactionManager() { }
    public DataSourceTransactionManager(DataSource dataSource) { }

    public void setDataSource(DataSource dataSource) { }
    public DataSource getDataSource() { }
    public void setEnforceReadOnly(boolean enforceReadOnly) { }
    public boolean isEnforceReadOnly() { }
}

JdbcTransactionManager

Alternative name for DataSourceTransactionManager, emphasizing JDBC focus.

public class JdbcTransactionManager extends DataSourceTransactionManager {
    public JdbcTransactionManager() { }
    public JdbcTransactionManager(DataSource dataSource) { }
}

DataSourceUtils

Helper methods for DataSource access with transaction support.

public abstract class DataSourceUtils {
    public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { }
    public static Connection doGetConnection(DataSource dataSource) throws SQLException { }
    public static void releaseConnection(Connection con, DataSource dataSource) { }
    public static void doReleaseConnection(Connection con, DataSource dataSource) throws SQLException { }
    public static boolean isConnectionTransactional(Connection con, DataSource dataSource) { }
    public static void applyTransactionTimeout(Statement stmt, DataSource dataSource) throws SQLException { }
}

Usage Examples

Declarative Transactions with @Transactional

import org.springframework.transaction.annotation.Transactional;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    private final JdbcTemplate jdbcTemplate;

    public UserService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Transactional
    public void createUser(String name, String email) {
        jdbcTemplate.update(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            name, email
        );

        jdbcTemplate.update(
            "INSERT INTO audit_log (action, details) VALUES (?, ?)",
            "USER_CREATED", "User: " + name
        );
        // Both operations committed together or rolled back
    }

    @Transactional(readOnly = true)
    public User findUser(Long id) {
        return jdbcTemplate.queryForObject(
            "SELECT id, name, email FROM users WHERE id = ?",
            (rs, rowNum) -> new User(
                rs.getLong("id"),
                rs.getString("name"),
                rs.getString("email")
            ),
            id
        );
    }
}

Transaction Configuration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

Programmatic Transactions with TransactionTemplate

import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.transaction.PlatformTransactionManager;

public class OrderService {
    private final JdbcTemplate jdbcTemplate;
    private final TransactionTemplate transactionTemplate;

    public OrderService(JdbcTemplate jdbcTemplate,
                       PlatformTransactionManager transactionManager) {
        this.jdbcTemplate = jdbcTemplate;
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public Long createOrder(Order order) {
        return transactionTemplate.execute(status -> {
            // Insert order
            KeyHolder keyHolder = new GeneratedKeyHolder();
            jdbcTemplate.update(
                con -> {
                    PreparedStatement ps = con.prepareStatement(
                        "INSERT INTO orders (user_id, total) VALUES (?, ?)",
                        Statement.RETURN_GENERATED_KEYS
                    );
                    ps.setLong(1, order.getUserId());
                    ps.setBigDecimal(2, order.getTotal());
                    return ps;
                },
                keyHolder
            );

            Long orderId = keyHolder.getKey().longValue();

            // Insert order items
            for (OrderItem item : order.getItems()) {
                jdbcTemplate.update(
                    "INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)",
                    orderId, item.getProductId(), item.getQuantity(), item.getPrice()
                );
            }

            return orderId;
        });
    }
}

Transaction Propagation

@Service
public class AccountService {

    @Transactional
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        // New transaction
        debit(fromId, amount);
        credit(toId, amount);
        recordTransaction(fromId, toId, amount);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void debit(Long accountId, BigDecimal amount) {
        // Joins existing transaction
        jdbcTemplate.update(
            "UPDATE accounts SET balance = balance - ? WHERE id = ?",
            amount, accountId
        );
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void recordTransaction(Long fromId, Long toId, BigDecimal amount) {
        // Always creates new transaction (independent)
        jdbcTemplate.update(
            "INSERT INTO transactions (from_account, to_account, amount) VALUES (?, ?, ?)",
            fromId, toId, amount
        );
    }
}

Transaction Isolation Levels

@Service
public class InventoryService {

    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void updateInventory(Long productId, int quantity) {
        // Prevents dirty reads
        Integer current = jdbcTemplate.queryForObject(
            "SELECT quantity FROM inventory WHERE product_id = ?",
            Integer.class,
            productId
        );

        jdbcTemplate.update(
            "UPDATE inventory SET quantity = ? WHERE product_id = ?",
            current + quantity,
            productId
        );
    }

    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void processOrder(Long orderId) {
        // Highest isolation level - prevents all concurrency issues
        // but with performance cost
    }
}

Rollback Rules

@Service
public class PaymentService {

    @Transactional(rollbackFor = PaymentException.class)
    public void processPayment(Payment payment) {
        jdbcTemplate.update(
            "INSERT INTO payments (order_id, amount, status) VALUES (?, ?, ?)",
            payment.getOrderId(),
            payment.getAmount(),
            "PROCESSING"
        );

        // Simulate payment processing
        if (!paymentGateway.process(payment)) {
            throw new PaymentException("Payment failed");
        }

        jdbcTemplate.update(
            "UPDATE payments SET status = ? WHERE order_id = ?",
            "COMPLETED",
            payment.getOrderId()
        );
    }

    @Transactional(noRollbackFor = ValidationException.class)
    public void validateAndSave(User user) {
        // Won't rollback for ValidationException
        if (!user.isValid()) {
            throw new ValidationException("Invalid user");
        }

        jdbcTemplate.update(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            user.getName(),
            user.getEmail()
        );
    }
}

Manual Rollback

@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        jdbcTemplate.update(
            "INSERT INTO orders (user_id, total) VALUES (?, ?)",
            order.getUserId(),
            order.getTotal()
        );

        if (order.getTotal().compareTo(BigDecimal.ZERO) < 0) {
            // Manually trigger rollback
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return;
        }

        // Continue processing
    }
}

Read-Only Transactions

@Service
public class ReportService {

    @Transactional(readOnly = true)
    public List<Report> generateReport() {
        // Read-only hint can improve performance
        // Some databases optimize read-only transactions

        List<Order> orders = jdbcTemplate.query(
            "SELECT * FROM orders WHERE created_at >= ?",
            new OrderRowMapper(),
            LocalDate.now().minusMonths(1)
        );

        // Process and return report
        return createReport(orders);
    }
}

Transaction Timeout

@Service
public class BatchService {

    @Transactional(timeout = 30)  // 30 seconds
    public void processBatch(List<Item> items) {
        for (Item item : items) {
            jdbcTemplate.update(
                "INSERT INTO processed_items (data) VALUES (?)",
                item.getData()
            );
        }
    }
}

Nested Transactions

@Service
public class OrderProcessingService {

    @Transactional
    public void processOrder(Long orderId) {
        // Outer transaction
        updateOrderStatus(orderId, "PROCESSING");

        try {
            // Inner transaction (REQUIRES_NEW)
            chargePayment(orderId);
        } catch (PaymentException e) {
            // Inner transaction rolled back
            // Outer transaction can continue
            updateOrderStatus(orderId, "PAYMENT_FAILED");
            return;
        }

        updateOrderStatus(orderId, "COMPLETED");
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void chargePayment(Long orderId) {
        // Independent transaction
        jdbcTemplate.update(
            "INSERT INTO payments (order_id, status) VALUES (?, ?)",
            orderId, "CHARGED"
        );
    }

    private void updateOrderStatus(Long orderId, String status) {
        jdbcTemplate.update(
            "UPDATE orders SET status = ? WHERE id = ?",
            status, orderId
        );
    }
}

TransactionAwareDataSourceProxy

@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource rawDataSource = new DriverManagerDataSource();
        rawDataSource.setUrl("jdbc:postgresql://localhost:5432/mydb");
        rawDataSource.setUsername("user");
        rawDataSource.setPassword("pass");

        // Wrap with transaction-aware proxy
        return new TransactionAwareDataSourceProxy(rawDataSource);
    }
}

Using DataSourceUtils

public class CustomDao {
    private final DataSource dataSource;

    public CustomDao(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void customOperation() {
        // Get connection (participates in Spring transactions)
        Connection con = DataSourceUtils.getConnection(dataSource);

        try {
            PreparedStatement ps = con.prepareStatement("SELECT * FROM users");
            ResultSet rs = ps.executeQuery();

            // Process results

            rs.close();
            ps.close();
        } catch (SQLException e) {
            throw new DataAccessException("Error", e) {};
        } finally {
            // Release connection (doesn't close if transactional)
            DataSourceUtils.releaseConnection(con, dataSource);
        }
    }
}

Testing with Transactions

@SpringBootTest
@Transactional  // Rolls back after each test
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void testCreateUser() {
        userService.createUser("Test User", "test@example.com");

        Integer count = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM users WHERE email = ?",
            Integer.class,
            "test@example.com"
        );

        assertEquals(1, count);
        // Transaction rolled back after test
    }

    @Test
    @Commit  // Explicitly commit after test
    public void testCreateUserCommit() {
        userService.createUser("Permanent User", "permanent@example.com");
        // Changes persisted
    }
}

Transaction Attributes

Common @Transactional attributes:

  • propagation: REQUIRED, REQUIRES_NEW, SUPPORTS, NOT_SUPPORTED, MANDATORY, NEVER, NESTED
  • isolation: DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
  • readOnly: true/false (optimization hint)
  • timeout: seconds before timeout
  • rollbackFor: Exceptions that trigger rollback
  • noRollbackFor: Exceptions that don't trigger rollback

Best Practices

  1. Use declarative transactions - @Transactional is simpler
  2. Set appropriate isolation level - Balance consistency and performance
  3. Keep transactions short - Minimize lock duration
  4. Use read-only transactions - For queries only
  5. Handle exceptions properly - Configure rollbackFor correctly
  6. Test transaction boundaries - Verify commit/rollback behavior
  7. Avoid transaction spanning layers - Keep in service layer
  8. Monitor transaction performance - Watch for long transactions