Spring JDBC integrates seamlessly with Spring's transaction management infrastructure, providing declarative and programmatic transaction support for JDBC operations.
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() { }
}Alternative name for DataSourceTransactionManager, emphasizing JDBC focus.
public class JdbcTransactionManager extends DataSourceTransactionManager {
public JdbcTransactionManager() { }
public JdbcTransactionManager(DataSource dataSource) { }
}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 { }
}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
);
}
}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);
}
}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;
});
}
}@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
);
}
}@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
}
}@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()
);
}
}@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
}
}@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);
}
}@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()
);
}
}
}@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
);
}
}@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);
}
}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);
}
}
}@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
}
}Common @Transactional attributes: