You can use JDBC with Spring through Spring’s JDBC support, especially JdbcTemplate. It removes much of the repetitive JDBC boilerplate such as opening connections, closing resources, handling PreparedStatement, iterating ResultSet, and translating SQLException into Spring’s DataAccessException hierarchy.
The typical setup is:
- Configure a
DataSource - Create a
JdbcTemplate - Inject it into a repository/DAO class
- Use it to run queries and updates
1. Add Spring JDBC and a database driver
For a Maven project, you usually need spring-jdbc and your database driver.
Example for PostgreSQL:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.2.8</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.7</version>
</dependency>
</dependencies>
If you use Spring Boot, you would usually use:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
plus the database driver.
2. Configure a DataSource
In plain Spring Java configuration, you can define a DataSource bean.
A common choice is HikariCP:
package org.kodejava.spring;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.time.Duration;
@Configuration
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/app");
config.setUsername("postgres");
config.setPassword("postgres");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(Duration.ofSeconds(5).toMillis());
config.setPoolName("AppHikariPool");
return new HikariDataSource(config);
}
}
You would also need the HikariCP dependency if you are not using Spring Boot:
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>6.3.0</version>
</dependency>
3. Create a JdbcTemplate bean
Spring can create JdbcTemplate from the configured DataSource.
package org.kodejava.spring;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Configuration
public class JdbcConfig {
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
If you are using Spring Boot, Boot usually autoconfigures JdbcTemplate for you as long as a DataSource exists.
4. Create a model class
For example, suppose you have a users table:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL
);
You can map rows to a Java object:
package org.kodejava.spring;
public class User {
private Long id;
private String name;
private String email;
public User() {
}
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
}
5. Use JdbcTemplate in a repository
A repository class can receive JdbcTemplate through constructor injection.
package org.kodejava.spring;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public User findById(Long id) {
String sql = """
SELECT id, name, email
FROM users
WHERE id = ?
""";
return jdbcTemplate.queryForObject(
sql,
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
),
id
);
}
public List<User> findAll() {
String sql = """
SELECT id, name, email
FROM users
ORDER BY id
""";
return jdbcTemplate.query(
sql,
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
)
);
}
public int insert(User user) {
String sql = """
INSERT INTO users (id, name, email)
VALUES (?, ?, ?)
""";
return jdbcTemplate.update(
sql,
user.getId(),
user.getName(),
user.getEmail()
);
}
public int update(User user) {
String sql = """
UPDATE users
SET name = ?, email = ?
WHERE id = ?
""";
return jdbcTemplate.update(
sql,
user.getName(),
user.getEmail(),
user.getId()
);
}
public int deleteById(Long id) {
String sql = "DELETE FROM users WHERE id = ?";
return jdbcTemplate.update(sql, id);
}
}
6. Enable component scanning
If you are using plain Spring, your configuration class should scan for repositories and services.
package org.kodejava.spring;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("org.kodejava.spring")
public class AppConfig {
}
Then you can bootstrap Spring:
package org.kodejava.spring;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class SpringJdbcExample {
public static void main(String[] args) {
try (AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class, DatabaseConfig.class, JdbcConfig.class)) {
UserRepository userRepository = context.getBean(UserRepository.class);
User user = new User(1L, "Alice", "[email protected]");
userRepository.insert(user);
User savedUser = userRepository.findById(1L);
System.out.println(savedUser.getName());
}
}
}
7. Handling query results safely
queryForObject() is convenient, but it throws an exception when no row is found. You can handle that explicitly:
package org.kodejava.spring;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Optional;
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Optional<User> findOptionalById(Long id) {
String sql = """
SELECT id, name, email
FROM users
WHERE id = ?
""";
try {
User user = jdbcTemplate.queryForObject(
sql,
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
),
id
);
return Optional.ofNullable(user);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
}
8. Using NamedParameterJdbcTemplate
For more readable SQL parameters, use NamedParameterJdbcTemplate.
package org.kodejava.spring;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class NamedUserRepository {
private final NamedParameterJdbcTemplate jdbcTemplate;
public NamedUserRepository(NamedParameterJdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public User findById(Long id) {
String sql = """
SELECT id, name, email
FROM users
WHERE id = :id
""";
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("id", id);
return jdbcTemplate.queryForObject(
sql,
params,
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
)
);
}
}
You can define it as a bean:
package org.kodejava.spring;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import javax.sql.DataSource;
@Configuration
public class NamedJdbcConfig {
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
}
9. Transactions
For multiple database operations that should succeed or fail together, use Spring transactions.
Add a transaction manager:
package org.kodejava.spring;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
@Bean
public TransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
Then use @Transactional in a service:
package org.kodejava.spring;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public void registerUser(User user) {
userRepository.insert(user);
// Other related database operations can go here.
// If a RuntimeException occurs, the transaction is rolled back.
}
}
10. Typical Spring Boot configuration
If you are using Spring Boot, the configuration is simpler.
application.properties:
spring.datasource.url=jdbc:postgresql://localhost:5432/app
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.hikari.maximum-pool-size=10
Repository:
package org.kodejava.spring;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public int countUsers() {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", Integer.class);
}
}
Summary
To use JDBC with Spring:
- Add
spring-jdbcand your database driver. - Configure a
DataSource. - Create or autoconfigure
JdbcTemplate. - Inject
JdbcTemplateinto repository classes. - Use
query(),queryForObject(), andupdate()for database operations. - Use
@Transactionalfor operations that need transaction boundaries.
For most applications, prefer JdbcTemplate over raw JDBC because it keeps the code shorter, safer, and easier to test.
