Where JPA Falls Short

In the Java ecosystem, JPA (Java Persistence API) is often the first tool we reach for when dealing with data. It allows us to map object-oriented domain models to databases and handle everything from simple CRUD operations to complex relationships through abstracted interfaces. For most business logic, managing entity lifecycles is sufficient.

However, there are always scenarios where JPA becomes cumbersome. This includes large-scale analytical queries requiring dozens of table joins, report generation using heavy database-specific functions, or bulk operations processing tens of thousands of records at once. While Querydsl can mitigate some of these issues, the generated queries may not be intuitive, or performance optimization opportunities might be limited.

In these moments, developers often turn back to JDBC (Java Database Connectivity). While JdbcTemplate has traditionally filled this role in Spring, its parameter binding and result mapping can feel verbose. In November 2023, Spring Boot 3.2 (Spring Framework 6.1) introduced a new interface to bridge this gap: JdbcClient.


JdbcClient: A Lightweight and Intuitive Interface

JdbcClient reimagines the functionality of the existing NamedParameterJdbcTemplate with a modern Fluent API style. It shares the same design philosophy as WebClient or RestClient, providing a functional approach to data access.

The standout feature is how it clearly expresses intent through method chaining. Instead of choosing from dozens of overloaded methods and filling in arguments, you step through the process sequentially, from defining the query to extracting the results.

Baseline Version

  • Spring Boot 3.2.0+ / Spring Framework 6.1.0+
  • Java 17+ (Optimized for Record support)
// Example usage of JdbcClient in Spring Boot 3.2
public List<ProductDto> findAvailableProducts(Long minPrice) {
    return jdbcClient.sql("SELECT id, name, price FROM products WHERE price >= :minPrice")
        .param("minPrice", minPrice)
        .query(ProductDto.class)
        .list();
}

The flow of defining SQL, binding parameters by name, and mapping results to a specific class feels natural and intuitive.


Under the Hood: How It Works

JdbcClient isn’t a ground-up rewrite of data access logic. Instead, it wraps Spring JDBC’s robust infrastructure.

1. A Proxy for NamedParameterJdbcTemplate

The default implementation, DefaultJdbcClient, holds an internal instance of NamedParameterJdbcTemplate. Methods like .sql(), .param(), and .update() eventually delegate to this internal template. This means it inherits the battle-tested transaction management and connection pooling mechanisms of the original.

2. Intelligent Result Mapping

When mapping results, JdbcClient analyzes the class type to automatically create an appropriate RowMapper. It uses DataClassRowMapper for Java Records and BeanPropertyRowMapper for standard Beans. Using the -parameters flag during compilation with Java 17+ allows it to read constructor parameter names at runtime and automatically match them with SQL column names.

3. Transaction Synchronization

JdbcClient participates in currently active transactions via Spring’s DataSourceUtils. This ensures that even if you modify a JPA entity and execute a query using JdbcClient within a single @Transactional block, they share the same database connection, guaranteeing atomicity.


Practical Use Cases

Let’s look at a few patterns for using JdbcClient effectively.

1. Flexible Parameter Binding

You can pass entire Map or DTO objects instead of individual parameters.

// Binding using a DTO object
SearchCondition condition = new SearchCondition("ELECTRONICS", 10000L);
jdbcClient.sql("SELECT ... WHERE category = :category AND price > :minPrice")
    .params(condition) // Automatically matches fields to parameter names
    .query(ProductDto.class)
    .list();

2. Retrieving Generated Keys (PK)

Retrieving ID values generated by the database after an insert is also straightforward.

GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
jdbcClient.sql("INSERT INTO products(name) VALUES (:name)")
    .param("name", "New Product")
    .update(keyHolder, "id");

Long newId = keyHolder.getKeyAs(Long.class);


Choosing the Right Tool

Each data access technology has distinct trade-offs. Choosing the right tool based on your project’s needs is crucial.

Feature JPA / Querydsl MyBatis JdbcTemplate JdbcClient
Abstraction Level High (Object-centric) Medium (SQL-centric) Low (JDBC Wrapper) Low (Modern Wrapper)
Productivity Excellent (Auto CRUD) Good (SQL Separation) Fair (Verbose Code) Excellent (Fluent API)
Complex Queries Difficult (Abstraction limits) Excellent (Direct Control) Excellent (Direct Control) Excellent (Direct Control)
Type Safety Very High (Compile-time) Low (Runtime XML) Low (String SQL) Low (String SQL)
Learning Curve High Medium Low Very Low

JdbcClient is the lightest option when you need direct SQL control without the heavy configuration of something like MyBatis.


Wrapping Up

JdbcClient in Spring Boot 3.2 is a modernization of a powerful existing toolset. It significantly improves Developer Experience (DX) by reducing boilerplate and making the logic’s intent clear.

Instead of trying to solve everything with JPA and getting stuck in a “query swamp,” you should flexibly mix in JdbcClient when appropriate. The goal isn’t to be dogmatic about a single technology but to choose the most efficient solution for the task at hand. This new interface is an excellent addition to any backend developer’s toolkit.


References