
Spring JdbcTemplate Example – Database Access
I’ll create a comprehensive technical blog post about Spring JdbcTemplate following your specifications.
Spring JdbcTemplate is a powerful abstraction layer that simplifies database operations in Spring applications by eliminating boilerplate JDBC code. Unlike raw JDBC connections that require manual resource management, exception handling, and SQL preparation, JdbcTemplate handles these concerns automatically while maintaining full control over SQL queries. This guide will walk you through practical implementations, common patterns, performance considerations, and troubleshooting techniques that’ll make your database layer both robust and maintainable.
How JdbcTemplate Works Under the Hood
JdbcTemplate operates as a wrapper around traditional JDBC operations, implementing the Template Method pattern to handle resource management and exception translation. When you execute a query, JdbcTemplate acquires a connection from the DataSource, prepares the statement, executes it, processes results, and ensures proper cleanup regardless of success or failure.
The magic happens through Spring’s exception translation mechanism, which converts checked SQLExceptions into Spring’s unchecked DataAccessException hierarchy. This means you’re dealing with more specific exceptions like DuplicateKeyException or DataIntegrityViolationException instead of generic SQL error codes.
// Traditional JDBC approach - lots of boilerplate
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setLong(1, userId);
rs = ps.executeQuery();
// process results...
} catch (SQLException e) {
// handle exception
} finally {
// cleanup resources manually
if (rs != null) rs.close();
if (ps != null) ps.close();
if (conn != null) conn.close();
}
// JdbcTemplate approach - clean and concise
User user = jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new Object[]{userId},
new UserRowMapper()
);
Step-by-Step Setup and Configuration
Setting up JdbcTemplate requires minimal configuration. Here’s a complete setup starting from dependency management through bean configuration.
First, add the necessary dependencies to your Maven pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.21</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.21</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
</dependencies>
Configure your DataSource and JdbcTemplate beans using Java configuration:
@Configuration
@ComponentScan(basePackages = "com.example.dao")
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/testdb");
config.setUsername("dbuser");
config.setPassword("dbpass");
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
return new HikariDataSource(config);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
JdbcTemplate template = new JdbcTemplate(dataSource);
template.setQueryTimeout(30);
template.setFetchSize(100);
return template;
}
}
Create a sample table and entity class for our examples:
-- Database schema
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) NOT NULL,
created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
active BOOLEAN DEFAULT TRUE
);
// Entity class
public class User {
private Long id;
private String username;
private String email;
private LocalDateTime createdDate;
private boolean active;
// constructors, getters, setters
public User() {}
public User(String username, String email) {
this.username = username;
this.email = email;
this.active = true;
}
// ... getter/setter methods
}
Real-World Implementation Examples
Let’s build a complete DAO implementation showcasing different JdbcTemplate operations you’ll encounter in production systems.
@Repository
public class UserDao {
private final JdbcTemplate jdbcTemplate;
public UserDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// Simple query returning single object
public User findById(Long id) {
String sql = "SELECT id, username, email, created_date, active FROM users WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id);
}
// Query returning list of objects
public List<User> findActiveUsers() {
String sql = "SELECT id, username, email, created_date, active FROM users WHERE active = ?";
return jdbcTemplate.query(sql, new UserRowMapper(), true);
}
// Insert operation with generated key retrieval
public Long createUser(User user) {
String sql = "INSERT INTO users (username, email, active) VALUES (?, ?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
ps.setString(1, user.getUsername());
ps.setString(2, user.getEmail());
ps.setBoolean(3, user.isActive());
return ps;
}, keyHolder);
return keyHolder.getKey().longValue();
}
// Batch insert for bulk operations
public void createUsers(List<User> users) {
String sql = "INSERT INTO users (username, email, active) VALUES (?, ?, ?)";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
User user = users.get(i);
ps.setString(1, user.getUsername());
ps.setString(2, user.getEmail());
ps.setBoolean(3, user.isActive());
}
@Override
public int getBatchSize() {
return users.size();
}
});
}
// Update operation
public int updateUserEmail(Long id, String newEmail) {
String sql = "UPDATE users SET email = ? WHERE id = ?";
return jdbcTemplate.update(sql, newEmail, id);
}
// Delete operation
public int deleteUser(Long id) {
String sql = "DELETE FROM users WHERE id = ?";
return jdbcTemplate.update(sql, id);
}
// Complex query with named parameters
public List<User> findUsersByEmailDomain(String domain) {
String sql = "SELECT id, username, email, created_date, active FROM users WHERE email LIKE ?";
return jdbcTemplate.query(sql, new UserRowMapper(), "%" + domain);
}
// Aggregate query
public int countActiveUsers() {
String sql = "SELECT COUNT(*) FROM users WHERE active = ?";
return jdbcTemplate.queryForObject(sql, Integer.class, true);
}
}
// RowMapper implementation for result set mapping
public class UserRowMapper implements RowMapper<User> {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
user.setCreatedDate(rs.getTimestamp("created_date").toLocalDateTime());
user.setActive(rs.getBoolean("active"));
return user;
}
}
Advanced Patterns and Best Practices
Production applications require more sophisticated patterns. Here’s how to handle complex scenarios effectively:
// Using NamedParameterJdbcTemplate for complex queries
@Repository
public class AdvancedUserDao {
private final NamedParameterJdbcTemplate namedJdbcTemplate;
public AdvancedUserDao(JdbcTemplate jdbcTemplate) {
this.namedJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
}
public List<User> findUsersByCriteria(UserSearchCriteria criteria) {
StringBuilder sql = new StringBuilder(
"SELECT id, username, email, created_date, active FROM users WHERE 1=1"
);
MapSqlParameterSource params = new MapSqlParameterSource();
if (criteria.getUsername() != null) {
sql.append(" AND username LIKE :username");
params.addValue("username", "%" + criteria.getUsername() + "%");
}
if (criteria.getEmailDomain() != null) {
sql.append(" AND email LIKE :emailDomain");
params.addValue("emailDomain", "%@" + criteria.getEmailDomain());
}
if (criteria.getCreatedAfter() != null) {
sql.append(" AND created_date > :createdAfter");
params.addValue("createdAfter", criteria.getCreatedAfter());
}
sql.append(" ORDER BY created_date DESC LIMIT :limit OFFSET :offset");
params.addValue("limit", criteria.getLimit());
params.addValue("offset", criteria.getOffset());
return namedJdbcTemplate.query(sql.toString(), params, new UserRowMapper());
}
// Transaction handling with @Transactional
@Transactional
public void transferUserData(Long fromUserId, Long toUserId) {
// Multiple operations that should be atomic
String deactivateFromUser = "UPDATE users SET active = false WHERE id = :fromId";
String updateToUser = "UPDATE users SET updated_date = CURRENT_TIMESTAMP WHERE id = :toId";
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("fromId", fromUserId);
params.addValue("toId", toUserId);
namedJdbcTemplate.update(deactivateFromUser, params);
namedJdbcTemplate.update(updateToUser, params);
// If any operation fails, the entire transaction rolls back
}
}
Performance Optimization and Monitoring
JdbcTemplate performance depends heavily on proper configuration and usage patterns. Here are key optimization strategies:
Operation Type | Best Practice | Performance Impact | Use Case |
---|---|---|---|
Single Insert | Use prepared statements with generated keys | Low overhead | <100 records |
Batch Insert | Use batchUpdate() with optimal batch size | 10-50x faster than individual inserts | 100-10,000 records |
Large Result Sets | Set appropriate fetchSize | Reduces memory usage by 70% | >1000 rows |
Complex Queries | Use NamedParameterJdbcTemplate | Better readability, minimal overhead | Dynamic queries |
// Performance monitoring and optimization
@Component
public class OptimizedUserDao {
private final JdbcTemplate jdbcTemplate;
private final MeterRegistry meterRegistry;
public OptimizedUserDao(JdbcTemplate jdbcTemplate, MeterRegistry meterRegistry) {
this.jdbcTemplate = jdbcTemplate;
this.meterRegistry = meterRegistry;
// Configure optimal settings
this.jdbcTemplate.setFetchSize(500); // Adjust based on memory constraints
this.jdbcTemplate.setMaxRows(10000); // Prevent runaway queries
}
public List<User> findUsersWithMetrics(int limit) {
Timer.Sample sample = Timer.start(meterRegistry);
try {
String sql = "SELECT id, username, email, created_date, active FROM users LIMIT ?";
List<User> users = jdbcTemplate.query(sql, new UserRowMapper(), limit);
meterRegistry.counter("user.query.success").increment();
return users;
} catch (Exception e) {
meterRegistry.counter("user.query.error").increment();
throw e;
} finally {
sample.stop(Timer.builder("user.query.duration")
.description("Time taken to query users")
.register(meterRegistry));
}
}
// Optimized batch processing with chunking
public void bulkInsertUsers(List<User> users) {
int batchSize = 1000; // Optimal batch size for most databases
String sql = "INSERT INTO users (username, email, active) VALUES (?, ?, ?)";
for (int i = 0; i < users.size(); i += batchSize) {
List<User> batch = users.subList(i, Math.min(i + batchSize, users.size()));
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int j) throws SQLException {
User user = batch.get(j);
ps.setString(1, user.getUsername());
ps.setString(2, user.getEmail());
ps.setBoolean(3, user.isActive());
}
@Override
public int getBatchSize() {
return batch.size();
}
});
}
}
}
Common Pitfalls and Troubleshooting
Even experienced developers encounter issues with JdbcTemplate. Here are the most common problems and their solutions:
- EmptyResultDataAccessException: Occurs when queryForObject() finds no results. Always handle this exception or use query() method instead.
- IncorrectResultSizeDataAccessException: Thrown when queryForObject() finds multiple results. Ensure your query returns exactly one row.
- Connection pool exhaustion: Monitor active connections and configure appropriate pool sizes. Use connection leak detection in development.
- Memory issues with large result sets: Set appropriate fetchSize and consider using streaming or pagination for large datasets.
// Robust error handling patterns
@Service
public class RobustUserService {
private final UserDao userDao;
// Handle empty results gracefully
public Optional<User> findUserById(Long id) {
try {
User user = userDao.findById(id);
return Optional.of(user);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
// Handle batch operation failures
public BatchResult createUsersInBatches(List<User> users) {
List<User> successful = new ArrayList<>();
List<String> errors = new ArrayList<>();
// Process in smaller batches to isolate failures
int batchSize = 100;
for (int i = 0; i < users.size(); i += batchSize) {
List<User> batch = users.subList(i, Math.min(i + batchSize, users.size()));
try {
userDao.createUsers(batch);
successful.addAll(batch);
} catch (DataAccessException e) {
// Log the error and continue with next batch
errors.add("Batch " + (i/batchSize + 1) + " failed: " + e.getMessage());
// Optionally, try individual inserts for failed batch
for (User user : batch) {
try {
userDao.createUser(user);
successful.add(user);
} catch (DataAccessException individualError) {
errors.add("Failed to create user " + user.getUsername() +
": " + individualError.getMessage());
}
}
}
}
return new BatchResult(successful, errors);
}
}
JdbcTemplate vs Alternatives Comparison
Understanding when to choose JdbcTemplate over other persistence technologies helps make informed architectural decisions:
Technology | Learning Curve | Performance | SQL Control | Best For |
---|---|---|---|---|
JdbcTemplate | Low | High | Full control | Complex queries, performance-critical apps |
JPA/Hibernate | Medium-High | Medium | Limited | Rapid development, standard CRUD operations |
MyBatis | Medium | High | Full control | Complex mappings, existing SQL codebase |
JOOQ | Medium | High | Type-safe SQL | Complex queries with compile-time safety |
For high-performance applications requiring fine-tuned SQL control, JdbcTemplate remains the optimal choice. It provides the perfect balance between raw JDBC power and Spring’s convenience features.
For comprehensive documentation and advanced configuration options, check the official Spring Framework Data Access documentation. The Spring JDBC source code also provides excellent examples of advanced usage patterns.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.