Talking to a relational database without the boilerplate — JdbcTemplate, result-set callbacks, and portable exceptions.
JdbcTemplate method to use for a query vs an update vs DDL; the callback interfaces that map
result sets (especially RowMapper); and Spring's DataAccessException hierarchy —
what it is, that it's unchecked, and why translation matters.
Plain JDBC forces you to open a connection, create a statement, loop the ResultSet, handle a
checked SQLException, and close everything in finally — the same ~15 lines around every
query. JdbcTemplate does all of that for you; you supply only the SQL and
the row-mapping logic.
| JdbcTemplate handles | You supply |
|---|---|
Opening & closing the Connection / Statement / ResultSet | The SQL string |
Iterating the ResultSet | A RowMapper (or other callback) |
Translating SQLException → DataAccessException | Query parameters |
Configure it with a DataSource (it's thread-safe once built):
@Bean
public JdbcTemplate jdbcTemplate(DataSource ds) {
return new JdbcTemplate(ds);
}
JdbcTemplate is a classic template-method + callback design: it runs the fixed workflow and
calls back into your code for the variable part. It acquires a Connection from the
DataSource lazily — when a query/update method is executed — and releases it afterwards, not
at construction.
| Method | Use for | Returns |
|---|---|---|
queryForObject(sql, rowMapper, args) | a query expecting one row | a single object |
queryForObject(sql, Integer.class, args) | a single scalar (e.g. a COUNT) | a single value |
query(sql, rowMapper, args) | a query returning many rows | a List<T> |
queryForList(sql, args) | rows as generic maps | List<Map> |
update(sql, args) | INSERT / UPDATE / DELETE | rows affected (int) |
execute(sql) | DDL (CREATE TABLE …) or arbitrary SQL | void / varies |
Read-only? the query* family. Change data? update. Schema/DDL? execute.
JdbcTemplate hands the raw ResultSet to a callback you provide:
| Callback | Method | Use when |
|---|---|---|
RowMapper<T> | mapRow(rs, rowNum) → one object per row | Most common — map each row to a domain object |
ResultSetExtractor<T> | extractData(rs) → one object for the whole result set | Aggregate many rows into one result |
RowCallbackHandler | processRow(rs) → returns void | Stream/side-effects, no collected result |
List<Account> accounts = jdbc.query(
"select id, name from account where active = ?",
(rs, rowNum) -> new Account(rs.getLong("id"), rs.getString("name")), // RowMapper
true);
JDBC's checked SQLException is cryptic and vendor-specific (error codes differ per database). Spring
translates it into a rich, portable hierarchy rooted at DataAccessException:
DataAccessException is the root — and it's a RuntimeException (unchecked), so you're not forced to catch it.DuplicateKeyException whatever the database, so your code isn't tied to one vendor's error codes.SQLExceptionTranslator; JdbcTemplate applies it automatically.DataIntegrityViolationException, DuplicateKeyException, BadSqlGrammarException, EmptyResultDataAccessException.queryForObject and "wrong number of rows"
Expecting exactly one row, queryForObject throws EmptyResultDataAccessException if it
finds none, and IncorrectResultSizeDataAccessException if it finds more than one — it
does not return null. A common exam catch.
Read "Using JdbcTemplate", "Running Queries", and "Consistent Exception Hierarchy".
This covers the whole JDBC section of your book — answer here rather than opening it. Options shuffle on every load.
RowMapper, or the difference between RowMapper and
ResultSetExtractor with code? Ask. Say "continue" for Lesson 9 — Transactions.