How does a custom RowMapper work with @Query in Spring Data JDBC, and when would you use one instead of the default mapping?
answer
- RowMapper.mapRow(rs, rowNum) -> one object per row
- rowMapperClass (no-arg) vs rowMapperRef (bean)
- mutually exclusive attributes
- default = EntityRowMapper via mapping context
- QueryMappingConfiguration = global per-type default
basics
~20 sA 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.
solid answer
~40 sBy default Spring Data JDBC maps a query's ResultSet columns onto your aggregate using its internal EntityRowMapper. When a hand-written @Query returns a shape that does not correspond to a mapped aggregate, for example a projection joining several tables or a computed DTO, you supply a Spring org.springframework.jdbc.core.RowMapper. Its mapRow(ResultSet rs, int rowNum) method reads columns and builds one object per row. You wire it via @Query(rowMapperClass = MyRowMapper.class), which must have a no-arg constructor, or @Query(rowMapperRef = "myRowMapperBean") to reference a Spring bean (useful when the mapper needs dependencies). rowMapperClass and rowMapperRef are mutually exclusive. You can also register a default RowMapper per return type globally by implementing QueryMappingConfiguration. Use a custom mapper for reporting rows, aggregations, or DTOs; rely on the default for plain aggregate loads.
code
java · 19 linespublic record OrderSummary(Long orderId, BigDecimal total, String customerName) {}
public class OrderSummaryRowMapper implements RowMapper<OrderSummary> {
@Override
public OrderSummary mapRow(ResultSet rs, int rowNum) throws SQLException {
return new OrderSummary(
rs.getLong("id"),
rs.getBigDecimal("total"),
rs.getString("last_name"));
}
}
public interface OrderRepository extends CrudRepository<Order, Long> {
@Query(value = "SELECT o.id, o.total, c.last_name " +
"FROM orders o JOIN customer c ON c.id = o.customer_id " +
"WHERE o.total > :min",
rowMapperClass = OrderSummaryRowMapper.class)
List<OrderSummary> findLargeOrders(@Param("min") BigDecimal min);
}go deeper
Know that a RowMapper turns a row into an object via mapRow.
Wire it with rowMapperClass vs rowMapperRef and know why you would use each; know the default EntityRowMapper exists.
Explain QueryMappingConfiguration for global defaults, ResultSetExtractor vs RowMapper, and null/label pitfalls.
Weigh custom mapping vs projection support, maintainability of hand SQL + mappers, and the loss of aggregate reconstruction guarantees.
## What a RowMapper is `org.springframework.jdbc.core.RowMapper<T>` is a core Spring JDBC functional interface: ```java @FunctionalInterface public interface RowMapper<T> { T mapRow(ResultSet rs, int rowNum) throws SQLException; } ``` It converts **one row** of a JDBC `ResultSet` into **one object** of type `T`. Spring calls it once per row as it iterates the result set; you read columns by label or index and construct your object. You do **not** call `rs.next()` yourself, Spring drives the cursor. ## Default mapping in Spring Data JDBC When you do not specify a mapper, Spring Data JDBC uses its own `EntityRowMapper` (built from the `RelationalMappingContext`). It matches result columns to the properties of the return-type aggregate using the configured `NamingStrategy` (e.g. `firstName` <-> `first_name`) and reconstructs the aggregate, including reading owned/referenced entities when it loads a full aggregate root. This is what powers derived queries and `CrudRepository` finders. ## When the default is not enough You need a custom `RowMapper` when the query result is **not** a straightforward aggregate load: - A **projection / DTO** combining columns from a join. - **Aggregated / computed** rows (`SUM`, `COUNT`, GROUP BY) that map to a report object, not an entity. - A shape whose columns do not align with any mapped aggregate's properties. ## Wiring a RowMapper to @Query The `@Query` annotation exposes two mutually exclusive attributes: - **`rowMapperClass`** — a `Class<? extends RowMapper>`. Spring instantiates it, so it needs an accessible **no-arg constructor**. - **`rowMapperRef`** — a **bean name** (String) resolved from the application context. Use this when the mapper needs injected collaborators, or when you want a single shared instance. ```java @Query(value = "SELECT o.id, o.total, c.last_name FROM orders o " + "JOIN customer c ON c.id = o.customer_id WHERE o.total > :min", rowMapperClass = OrderSummaryRowMapper.class) List<OrderSummary> findLargeOrders(@Param("min") BigDecimal min); ``` Setting **both** attributes is a configuration error. There is a parallel `resultSetExtractorClass`/`resultSetExtractorRef` pair when you need to map the **entire** ResultSet to one object (a `ResultSetExtractor`), rather than row-by-row. ## Global default mappers: QueryMappingConfiguration Instead of naming a mapper on every method, you can register default `RowMapper`s per return type by providing a `org.springframework.data.jdbc.repository.QueryMappingConfiguration` bean. Spring Data JDBC consults it to find a mapper for a given type when a method does not specify one. ## Gotchas - **No-arg constructor** is required for `rowMapperClass`; if your mapper needs dependencies, use `rowMapperRef` with a registered bean. - **Column labels**: read by the label your SQL projects (use `AS` aliases to make them stable); mismatches throw `SQLException` / `InvalidResultSetAccessException`. - **Null handling**: `rs.getLong` returns 0 for SQL NULL; use `rs.getObject(col, Long.class)` or check `rs.wasNull()` when nullability matters. - **One row = one object**: for whole-result mapping (e.g. building a single Map from many rows) use a `ResultSetExtractor`, not a `RowMapper`. - A custom mapper **bypasses** aggregate reconstruction, so referenced child entities are not auto-loaded; you map exactly what the SQL returns.
- Your RowMapper needs an injected Spring bean. Which @Query attribute do you use and why?rowMapperRef with the mapper's bean name. rowMapperClass is instantiated reflectively via a no-arg constructor and cannot receive dependencies, whereas rowMapperRef resolves an existing, dependency-injected bean from the context.
- What is the difference between a RowMapper and a ResultSetExtractor?RowMapper maps one row to one object and is invoked per row (Spring drives the cursor). ResultSetExtractor is handed the whole ResultSet and returns a single result, letting you aggregate across rows (e.g. build one Map or a single object from many rows). @Query supports resultSetExtractorClass/Ref for the latter.
saying these in an interview costs you the question
- Calling rs.next() inside mapRow (Spring already advances the cursor)
- Setting both rowMapperClass and rowMapperRef
- Expecting rowMapperClass with a constructor that needs dependencies to work
- Assuming a custom RowMapper still auto-loads referenced child entities