Introduction
Spring Framework 6.1 introduced JdbcClient
, a modern and fluent API for database access that simplifies working with JDBC. If you’ve been using JdbcTemplate
for years, JdbcClient
will feel familiar yet refreshingly easier to work with.
This guide will walk you through everything you need to know about JdbcClient
, from basic queries to advanced features.
All code examples in this guide are available in the accompanying GitHub repository: https://github.com/theautonomy/jdbcclient-example.git |
Why JdbcClient?
Traditional JdbcTemplate
has served us well, but JdbcClient
offers several improvements:
-
Fluent API: Chain method calls for more readable code
-
Simplified parameter binding: Named parameters without the verbose
MapSqlParameterSource
-
Better type inference: More intuitive result mapping
-
Modern Java support: Takes advantage of recent Java features
Getting Started
Dependencies
First, add Spring JDBC to your project. For Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>3.5.6</version>
</dependency>
For Gradle:
implementation 'org.springframework.boot:spring-boot-starter-jdbc:3.5.6'
Basic Configuration
Spring Boot auto-configures JdbcClient
for you. Simply inject it into your components:
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Repository;
@Repository
public class CustomerRepository {
private final JdbcClient jdbcClient;
public CustomerRepository(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
}
If you’re not using Spring Boot, create a JdbcClient bean manually using JdbcClient.create(dataSource) .
|
Basic Operations
Simple Queries
Let’s start with the most common operation: querying data.
public class CustomerRepository {
private final JdbcClient jdbcClient;
public CustomerRepository(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
public List<Customer> findAll() {
return jdbcClient.sql("SELECT id, name, email FROM customers")
.query(Customer.class)
.list();
}
public Optional<Customer> findById(Long id) {
return jdbcClient.sql("SELECT id, name, email FROM customers WHERE id = :id")
.param("id", id)
.query(Customer.class)
.optional();
}
}
Notice how clean this is compared to JdbcTemplate
. No RowMapper
boilerplate needed for simple cases!
Parameter Binding
JdbcClient
makes parameter binding straightforward with named parameters:
public List<Customer> findByNameAndStatus(String name, String status) {
return jdbcClient.sql("""
SELECT id, name, email, status
FROM customers
WHERE name LIKE :name AND status = :status
""")
.param("name", "%" + name + "%")
.param("status", status)
.query(Customer.class)
.list();
}
You can also use a Map
for multiple parameters:
public List<Customer> findByMultipleCriteria(Map<String, Object> params) {
return jdbcClient.sql("""
SELECT id, name, email
FROM customers
WHERE status = :status
AND created_date > :createdAfter
""")
.params(params)
.query(Customer.class)
.list();
}
Insert Operations
Inserting data is just as elegant:
public void createCustomer(Customer customer) {
jdbcClient.sql("""
INSERT INTO customers (name, email, status)
VALUES (:name, :email, :status)
""")
.param("name", customer.getName())
.param("email", customer.getEmail())
.param("status", customer.getStatus())
.update();
}
The update() method returns the number of rows affected.
|
Retrieving Generated Keys
When inserting records with auto-generated IDs:
public Long createCustomerAndReturnId(Customer customer) {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcClient.sql("""
INSERT INTO customers (name, email, status)
VALUES (:name, :email, :status)
""")
.param("name", customer.getName())
.param("email", customer.getEmail())
.param("status", customer.getStatus())
.update(keyHolder);
Number key = (Number)keyHolder.getKeys().get("ID");
return key.longValue();
}
Update and Delete
Updates and deletes follow the same pattern:
public int updateCustomerEmail(Long id, String newEmail) {
return jdbcClient.sql("""
UPDATE customers
SET email = :email
WHERE id = :id
""")
.param("email", newEmail)
.param("id", id)
.update();
}
public int deleteCustomer(Long id) {
return jdbcClient.sql("DELETE FROM customers WHERE id = :id")
.param("id", id)
.update();
}
Advanced Features
Custom Row Mapping
For complex mapping scenarios, use a custom RowMapper
:
public List<CustomerDTO> findCustomersWithOrders() {
return jdbcClient.sql("""
SELECT c.id, c.name, c.email, COUNT(o.id) as order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name, c.email
""")
.query((rs, rowNum) -> new CustomerDTO(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email"),
rs.getInt("order_count")
))
.list();
}
Working with Single Values
Querying for a single scalar value:
public Long countActiveCustomers() {
return jdbcClient.sql("SELECT COUNT(*) FROM customers WHERE status = :status")
.param("status", "ACTIVE")
.query(Long.class)
.single();
}
public Optional<String> findCustomerEmail(Long id) {
return jdbcClient.sql("SELECT email FROM customers WHERE id = :id")
.param("id", id)
.query(String.class)
.optional();
}
Transaction Management
JdbcClient
works seamlessly with Spring’s transaction management:
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final JdbcClient jdbcClient;
public OrderService(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
@Transactional
public void processOrder(Order order) {
// Insert order
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcClient.sql("""
INSERT INTO orders (customer_id, status, total_amount)
VALUES (:customerId, :status, :totalAmount)
""")
.param("customerId", order.getCustomerId())
.param("status", order.getStatus())
.param("totalAmount", order.getTotalAmount())
.update(keyHolder);
Number key = (Number) keyHolder.getKeys().get("ID");
Long orderId = key.longValue();
// Insert order items
for (OrderItem item : order.getItems()) {
jdbcClient.sql("""
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (:orderId, :productId, :quantity, :price)
""")
.param("orderId", orderId)
.param("productId", item.getProductId())
.param("quantity", item.getQuantity())
.param("price", item.getPrice())
.update();
}
// Update inventory
jdbcClient.sql("""
UPDATE products
SET stock = stock - :quantity
WHERE id = :productId
""")
.param("quantity", order.getTotalQuantity())
.param("productId", order.getProductId())
.update();
}
}
Best Practices
Use Text Blocks for SQL
Java text blocks make SQL queries more readable:
// Good - using text blocks
public List<Customer> searchCustomers(String criteria) {
return jdbcClient.sql("""
SELECT c.id, c.name, c.email, c.status
FROM customers c
WHERE c.name LIKE :criteria
OR c.email LIKE :criteria
ORDER BY c.created_at DESC
""")
.param("criteria", "%" + criteria + "%")
.query(Customer.class)
.list();
}
// Avoid - concatenated strings
public List<Customer> searchCustomersOldWay(String criteria) {
return jdbcClient.sql("SELECT c.id, c.name, c.email, c.status " +
"FROM customers c " +
"WHERE c.name LIKE :criteria OR c.email LIKE :criteria " +
"ORDER BY c.created_at DESC")
.param("criteria", "%" + criteria + "%")
.query(Customer.class)
.list();
}
Handle Empty Results Gracefully
Always consider that queries might return no results:
public Customer getCustomerById(Long id) {
return jdbcClient.sql("SELECT id, name, email FROM customers WHERE id = :id")
.param("id", id)
.query(Customer.class)
.optional()
.orElseThrow(() -> new CustomerNotFoundException(id));
}
Use Records for DTOs
Java records are perfect for query results:
public record CustomerSummary(
Long id,
String name,
String email,
int orderCount,
BigDecimal totalSpent
) {}
public List<CustomerSummary> getCustomerSummaries() {
return jdbcClient.sql("""
SELECT
c.id,
c.name,
c.email,
COUNT(o.id) as orderCount,
COALESCE(SUM(o.total_amount), 0) as totalSpent
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name, c.email
""")
.query(CustomerSummary.class)
.list();
}
Keep SQL in Repository Layer
Encapsulate database access in repository classes:
@Repository
public class CustomerRepository {
private final JdbcClient jdbcClient;
public CustomerRepository(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
public List<Customer> findAll() { /* ... */ }
public Optional<Customer> findById(Long id) { /* ... */ }
public void save(Customer customer) { /* ... */ }
public void update(Customer customer) { /* ... */ }
public void delete(Long id) { /* ... */ }
}
Migration from JdbcTemplate
If you’re migrating from JdbcTemplate
, here’s a comparison:
JdbcTemplate |
---|
|
|
JdbcClient |
---|
|
|
JdbcClient doesn’t replace JdbcTemplate . Both APIs coexist in Spring Framework, and you can use whichever fits your needs better.
|
Common Pitfalls
Forgetting to Call Terminal Operations
// Wrong - query never executes!
jdbcClient.sql("SELECT * FROM customers")
.param("status", "ACTIVE");
// Correct - must call a terminal operation
List<Customer> customers = jdbcClient.sql("SELECT * FROM customers")
.param("status", "ACTIVE")
.query(Customer.class)
.list();
Mixing Positional and Named Parameters
// Wrong - can't mix ? and :name
jdbcClient.sql("SELECT * FROM customers WHERE id = ? AND status = :status")
.param("status", "ACTIVE")
.query(Customer.class)
.list();
// Correct - use named parameters consistently
jdbcClient.sql("SELECT * FROM customers WHERE id = :id AND status = :status")
.param("id", 123L)
.param("status", "ACTIVE")
.query(Customer.class)
.list();
Conclusion
JdbcClient
brings a modern, fluent API to Spring JDBC that makes database access code cleaner and more maintainable. Its main advantages are:
-
Simplified syntax with method chaining
-
Better handling of named parameters
-
Reduced boilerplate code
-
Seamless integration with existing Spring features
Whether you’re starting a new project or maintaining existing code, JdbcClient
is worth considering for your database access layer. It provides all the power of JdbcTemplate
with a more developer-friendly API.
About the Author
Wei Li is a passionate software developer and technical writer specializing in Java and Spring Framework. With years of experience in building enterprise applications, Wei Li enjoys sharing knowledge through detailed tutorials and practical guides. For questions or feedback, feel free to reach out at weili.mail@gmail.com. |