JdbcClient: 3 Ways to Bind Multiple Placeholders 2025
Discover 3 modern ways to bind multiple placeholders with Spring's JdbcClient in 2025. Learn positional, named, and POJO binding for clean, efficient database code.
Daniel Weber
Senior Java developer specializing in Spring Framework, data access, and modern application architecture.
Introduction: The Evolution from JdbcTemplate to JdbcClient
For years, Spring developers have relied on JdbcTemplate
for direct JDBC access. It was a massive improvement over raw JDBC, handling resource management and simplifying common operations. However, its API could sometimes feel verbose and less intuitive. Enter JdbcClient
, the modern, fluent alternative introduced in Spring Framework 6.1 (part of Spring Boot 3.2).
In 2025, writing clean, maintainable, and efficient data access code is non-negotiable. JdbcClient
provides a streamlined, builder-style API that makes database interactions more readable and less error-prone. One of the most common tasks is executing a query with multiple conditions, which requires binding several placeholder values. While it seems simple, the way you bind these values can significantly impact your code's quality.
This post dives deep into the three primary ways to bind multiple placeholders using JdbcClient
, helping you choose the right approach for any situation.
Setting the Stage: Prerequisites
To follow along with the examples, you'll need a project with the following dependencies. A simple Spring Boot 3.2+ setup is perfect.
Maven Dependencies (pom.xml):
<dependencies>
<!-- Core Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- H2 In-Memory Database for demonstration -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
We'll assume you have a JdbcClient
instance autowired in your service or repository and a simple users
table.
// A simple User record for mapping results
public record User(Long id, String username, String role, String status) {}
@Repository
public class UserRepository {
private final JdbcClient jdbcClient;
public UserRepository(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
// ... methods will go here
}
Method 1: The Classic - Positional Parameters (?)
What Are Positional Parameters?
This is the traditional JDBC approach. You use a question mark (?
) as a placeholder in your SQL query for each parameter. The JdbcClient
then binds the values you provide in the exact order they appear in the query. It's straightforward and gets the job done for simple queries.
Code Example: Using Positional Parameters
Imagine you want to find all users who have the role 'ADMIN' and are in 'ACTIVE' status. The SQL query uses two ?
placeholders.
public List<User> findUsersByRoleAndStatusPositional(String role, String status) {
String sql = "SELECT * FROM users WHERE role = ? AND status = ?";
return jdbcClient.sql(sql)
.params(role, status) // Parameters are bound in order
.query(User.class)
.list();
}
In this example, the .params(role, status)
method takes a varargs list of objects. The first argument (role
) is bound to the first ?
, and the second argument (status
) is bound to the second ?
. Simple and direct.
Pros and Cons
- Pros:
- Simplicity: Very easy to understand for queries with one or two parameters.
- Familiarity: Anyone who has used raw JDBC or
JdbcTemplate
will recognize this pattern immediately. - Concise: The code is compact for simple use cases.
- Cons:
- Error-Prone: The biggest drawback. If you accidentally switch the order of parameters in the
.params()
call (e.g.,.params(status, role)
), you'll get a logical error that the compiler won't catch. This can lead to subtle, hard-to-find bugs. - Poor Readability: As the number of parameters grows, it becomes difficult to match the
?
placeholders to their corresponding values. A query with five or more?
is a maintenance nightmare.
- Error-Prone: The biggest drawback. If you accidentally switch the order of parameters in the
Method 2: The Readable Choice - Named Parameters (:name)
What Are Named Parameters?
Named parameters, a concept popularized by NamedParameterJdbcTemplate
, are a massive step up in terms of code clarity. Instead of anonymous ?
placeholders, you use descriptive names prefixed with a colon, such as :role
or :status
. You then bind values to these specific names, making the query self-documenting and eliminating any ambiguity about parameter order.
Code Example: Using Named Parameters
Let's rewrite our previous query using named parameters. You can bind them one by one or by passing a Map
.
Binding one by one with .param()
:
public List<User> findUsersByRoleAndStatusNamed(String role, String status) {
String sql = "SELECT * FROM users WHERE role = :role AND status = :status";
return jdbcClient.sql(sql)
.param("role", role)
.param("status", status)
.query(User.class)
.list();
}
Here, the order of the .param()
calls doesn't matter. You could call .param("status", status)
first, and the result would be identical. The code is explicit and far easier to read.
Binding with a Map
:
public List<User> findUsersByRoleAndStatusNamedWithMap(String role, String status) {
String sql = "SELECT * FROM users WHERE role = :role AND status = :status";
Map<String, Object> params = Map.of("role", role, "status", status);
return jdbcClient.sql(sql)
.params(params)
.query(User.class)
.list();
}
Pros and Cons
- Pros:
- Excellent Readability: The SQL is self-documenting. It's immediately clear which value maps to which part of the
WHERE
clause. - Maintainable: Refactoring or adding new parameters is much safer, as you're not dependent on a fragile order.
- Less Error-Prone: Eliminates the parameter order bugs common with positional placeholders.
- Excellent Readability: The SQL is self-documenting. It's immediately clear which value maps to which part of the
- Cons:
- Slightly More Verbose: Requires typing out the parameter names, which adds a few more characters to your code compared to the positional approach. This is a very minor trade-off for the massive gain in clarity.
Method 3: The Modern Standard - POJO/Bean Property Binding
What is POJO Binding?
This is arguably the most powerful and 'Spring-like' way to handle parameters, especially for complex queries. The .paramBean()
method allows you to pass an entire Java object (a POJO, a JavaBean, or a Java Record
) as the source of your parameters. JdbcClient
automatically inspects the object's properties and maps them to named parameters in your SQL that share the same name.
This approach promotes creating dedicated Data Transfer Objects (DTOs) or filter objects for your queries, leading to cleaner, more organized, and type-safe code.
Code Example: Using .paramBean()
First, let's define a simple Record
to hold our query filter criteria. Records are perfect for this as they are immutable data carriers.
// A dedicated filter object for our query
public record UserFilter(String role, String status) {}
Now, our repository method becomes incredibly clean:
public List<User> findUsers(UserFilter filter) {
String sql = "SELECT * FROM users WHERE role = :role AND status = :status";
return jdbcClient.sql(sql)
.paramBean(filter) // Pass the entire filter object!
.query(User.class)
.list();
}
JdbcClient
sees the UserFilter
object, finds its properties (role
and status
), and automatically binds their values to the :role
and :status
named parameters in the SQL. It's clean, efficient, and highly scalable. If you need to add a third filter criterion, you simply add it to the UserFilter
record and your SQL query; the binding logic remains a single line.
Pros and Cons
- Pros:
- Maximum Readability and Cleanliness: Encapsulates all query parameters into a single, well-defined object.
- Highly Type-Safe: You're working with a strongly-typed filter object, reducing the chance of type-mismatch errors.
- Excellent for Complex Queries: This method shines when you have many optional search criteria. You can build the SQL dynamically and pass a single bean that contains all potential parameters.
- Promotes Good Design: Encourages the use of DTOs and a clear separation between your business logic and data access layer.
- Cons:
- Requires an Extra Class: You need to define a POJO or Record for your parameters. This might feel like slight overhead for a query with only one or two parameters, but the long-term benefits usually outweigh this.
Quick Comparison: Positional vs. Named vs. POJO
Feature | Positional (?) | Named (:name) | POJO/Bean (.paramBean) |
---|---|---|---|
Readability | Low | High | Very High |
Maintainability | Low | High | Very High |
Type Safety | Low (runtime errors) | Medium | High (compile-time safety) |
Verbosity | Low | Medium | Low (in the method call) |
Best Use Case | Quick, simple queries with 1-2 params. | Most general-purpose queries. | Complex queries, reusable filter logic. |
Conclusion: Best Practices for 2025 and Beyond
The introduction of JdbcClient
has modernized direct database access in Spring. When it comes to binding multiple parameters, you have a clear progression of options, each with its place.
Here's our recommendation for 2025:
- Default to Named Parameters: For the vast majority of your queries, named parameters (
:name
) should be your go-to choice. They offer the best balance of readability, safety, and simplicity. - Embrace POJO Binding for Complexity: When a query involves three or more parameters, or when the query logic is part of a complex search feature, immediately switch to the
.paramBean()
method. Creating a dedicated filter object will pay dividends in maintainability and code clarity down the line. - Use Positional Parameters Sparingly: Reserve positional parameters (
?
) for trivial, one-off queries where the risk of error is minimal. Think of it as a quick tool for a simple job, not a foundation for your application's data access layer.
By thoughtfully choosing your parameter binding strategy, you can leverage the full power of JdbcClient
to write database code that is not only functional but also robust, readable, and a pleasure to maintain.