skip to content

JDBC @Query & Templates

@Query in Spring Data JDBC is plain SQL, results are mapped with a RowMapper, and JdbcAggregateTemplate covers programmatic access. A good answer when you want predictable SQL and no ORM surprises.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

In Spring Data JDBC, what language does the @Query annotation expect, and how does that differ from Spring Data JPA?

level: juniorimportance: must knowfreq 70%

answer

  1. JDBC @Query = raw SQL, no JPQL
  2. JPA @Query defaults to JPQL, nativeQuery=true for SQL
  3. table/column names, not entity fields
  4. :named params + @Param
  5. writes need @Modifying

basics

~10 s

@Query in Spring Data JDBC takes plain, database-specific SQL. Spring Data JPA's @Query defaults to JPQL (an entity-oriented query language). Spring Data JDBC has no JPQL, so you always write real SQL.

solid answer

~40 s

Spring Data JDBC's @Query holds raw, dialect-specific SQL that runs against the database as written, there is no JPQL and no object query language. This is a core difference from Spring Data JPA, whose @Query defaults to JPQL (nativeQuery=true switches JPA to SQL). Because Spring Data JDBC is a thin, no-JPA mapper, you reference real table and column names, not entity fields. Parameters are bound by name with :paramName and matched via method arguments annotated @Param. Queries that modify data (UPDATE/DELETE/INSERT) additionally need @Modifying. Read queries map the ResultSet back to your aggregate automatically, or to a type you control via a RowMapper. The trade-off: you gain full SQL power (window functions, CTEs, vendor features) but lose database portability, since the SQL is tied to your specific dialect.

code

java · 15 lines
java
public interface CustomerRepository extends CrudRepository<Customer, Long> {

    // Plain SQL, references the `customer` table and `last_name` column
    @Query("SELECT * FROM customer WHERE last_name = :lastName")
    List<Customer> findByLastName(@Param("lastName") String lastName);

    // A modifying query must be annotated @Modifying
    @Modifying
    @Query("UPDATE customer SET active = false WHERE last_login < :cutoff")
    int deactivateStaleAccounts(@Param("cutoff") java.time.LocalDate cutoff);

    // Scalar result
    @Query("SELECT count(*) FROM customer WHERE active = true")
    long countActive();
}

go deeper

for a junior

Know that Spring Data JDBC @Query is real SQL, not JPQL, and JPA's is the opposite.

for a middle

Add named-parameter binding with @Param and the @Modifying requirement for writes.

for a senior

Discuss portability trade-offs, derived-vs-@Query, and when to reach for hand-written SQL over generated queries.

for a principal

Frame the no-JPA design: transparency and full SQL power vs. loss of dialect portability and manual aggregate reconstruction responsibilities.

## Background **Spring Data JDBC** is a persistence module that maps *aggregates* (an aggregate root plus the entities it owns) to relational tables using a lightweight mapping layer, deliberately *without* JPA, Hibernate, lazy loading, a persistence context, or dirty checking. Because there is no JPA behind it, there is also **no JPQL** (Java Persistence Query Language, the entity-field-based query language JPA compiles into SQL). ## What `@Query` expects The annotation is `org.springframework.data.jdbc.repository.query.Query`. Its `value` attribute is a **plain SQL string** sent to the database essentially as written. You reference **table names and column names**, not Java field names. Example: ```java @Query("SELECT * FROM customer WHERE last_name = :lastName") List<Customer> findByLastName(@Param("lastName") String lastName); ``` Contrast with Spring Data JPA, where `@Query("select c from Customer c where c.lastName = :lastName")` is **JPQL** referencing the *entity* `Customer` and its *field* `lastName`; JPA then translates it to SQL. In JPA you opt into raw SQL with `@Query(value = "...", nativeQuery = true)`. In Spring Data JDBC there is no such flag because **everything is native SQL already**. ## When you need `@Query` Spring Data JDBC still supports **derived query methods** (`findByLastName(String)`), where the method name is parsed and SQL is generated for you. You reach for `@Query` when the derived approach cannot express what you need: joins, aggregations, GROUP BY, window functions, CTEs, vendor-specific syntax, or hand-tuned SQL. ## Parameter binding - **Named parameters** `:name` are the norm; bind each with a `@Param("name")` method argument (the `@Param` name may be omitted if you compile with `-parameters`). - Positional `?` placeholders are generally avoided; named binding is idiomatic and safe. - Binding is done through `NamedParameterJdbcTemplate` under the hood, so values are passed as JDBC bind parameters, which prevents SQL injection. **Never** string-concatenate user input into the SQL. ## Modifying queries A `@Query` that performs INSERT/UPDATE/DELETE must also be annotated `@Modifying` (`org.springframework.data.jdbc.repository.query.Modifying` / the Spring Data `@Modifying`). The method should return `void`, `int`/`long` (rows affected), or `boolean` (any row affected). ```java @Modifying @Query("UPDATE customer SET active = false WHERE last_login < :cutoff") int deactivateStaleAccounts(@Param("cutoff") LocalDate cutoff); ``` ## Result mapping For SELECTs, the returned `ResultSet` is mapped back to the return type. By default Spring Data JDBC uses its own entity mapper (matching columns to the aggregate's properties). You can override mapping with a custom `RowMapper` via `@Query(rowMapperClass = ...)` or `rowMapperRef = ...`. Return types can be a single entity, `Optional`, `List`, `Stream`, a scalar (e.g. `long` for a COUNT), or a DTO/projection when you supply a RowMapper. ## Gotchas - **No JPQL / no HQL**: writing `SELECT c FROM Customer c` will fail, it is not SQL. - **Column vs field names**: use `last_name` (the column), not `lastName`, unless your naming strategy maps them. - **Portability**: SQL is dialect-bound; moving from H2 to Postgres can break vendor-specific queries. - **`@Modifying` required** for writes; forgetting it causes the query to be treated as a read and fail or behave unexpectedly. - **No automatic loading of referenced entities** for a custom `@Query`: if you hand-write SQL, you are responsible for what columns come back; the default aggregate mapping only fully reconstructs the aggregate when the SELECT returns the expected columns.

  • How do you bind method arguments into a Spring Data JDBC @Query?
    With named parameters :name in the SQL, matched to method arguments annotated @Param("name") (the name can be omitted when compiled with -parameters). Values are passed as JDBC bind parameters via NamedParameterJdbcTemplate, so they are injection-safe.
  • What must you add to a @Query that performs an UPDATE or DELETE?
    The @Modifying annotation. Return void, int/long (rows affected), or boolean. Without @Modifying the framework treats it as a SELECT.

saying these in an interview costs you the question

  • Claiming Spring Data JDBC @Query uses JPQL or needs nativeQuery=true
  • Referencing entity field names instead of table/column names
  • Forgetting @Modifying on write queries
  • String-concatenating user input into the SQL instead of using :named parameters

context

open as a page

How do parameter binding and @Modifying work for @Query methods in Spring Data JDBC, including collection parameters and return types?

level: middleimportance: should knowfreq 45%

basics

~10 s

Bind arguments with named parameters :name plus @Param. A collection bound to an IN (:ids) clause expands to multiple placeholders. Write queries need @Modifying and return void, int/long (rows affected), or boolean.

open as a page

How does a custom RowMapper work with @Query in Spring Data JDBC, and when would you use one instead of the default mapping?

level: middleimportance: should knowfreq 50%

basics

~20 s

A RowMapper turns one ResultSet row into an object via mapRow(rs, rowNum). You attach it with @Query(rowMapperClass = X.class) or rowMapperRef="beanName". Use one when the default entity mapping cannot produce your shape, like a custom DTO from a join.

open as a page

What is JdbcAggregateTemplate and when would you use it instead of a repository?

level: seniorimportance: should knowfreq 40%

basics

~20 s

JdbcAggregateTemplate is the lower-level engine behind Spring Data JDBC repositories. It offers programmatic aggregate operations, insert, update, save, delete, findById, findAll, count, without declaring a repository interface. Use it for dynamic or fine-grained control over persistence.

open as a page

Compare @Query repositories, JdbcAggregateTemplate, and NamedParameterJdbcTemplate. When do you choose each, and what are the architectural trade-offs?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

Use @Query repositories for declarative CRUD and hand SQL reads. Use JdbcAggregateTemplate for programmatic aggregate operations and explicit insert/update control. Use NamedParameterJdbcTemplate for raw SQL that has no aggregate mapping, like reports or bulk operations.

open as a page