JDBC, JPA & Spring Data
Spring offers a ladder of data-access abstractions.
Introduction
Spring offers a ladder of data-access abstractions. Pick the lowest rung that solves your problem; you can always climb. All three styles share a unified DataAccessException hierarchy, so error handling stays consistent no matter which one you choose.
Understanding the topic
The ladder:
JdbcTemplate— thin wrapper over JDBC. SQL stays explicit; boilerplate disappears. Best for reports, bulk loads and performance-critical paths.- Spring ORM — integrates Hibernate / JPA: manage entities, let the ORM emit SQL. Best for rich domain models with relationships.
- Spring Data JPA — declare a repository interface, get queries generated from method names. Best for CRUD-heavy domains where 80% of queries are obvious.
Syntax reference
The same query, three abstraction levels:
// 1. JdbcTemplate — you own the SQLList<Order> orders = jdbc.query("select id, total from orders where customer_id = ?",(rs, i) -> new Order(rs.getLong("id"), rs.getBigDecimal("total")),customerId);// 2. JPA EntityManager — JPQL, ORM emits SQLList<Order> orders = em.createQuery("select o from Order o where o.customer.id = :id", Order.class).setParameter("id", customerId).getResultList();// 3. Spring Data — derive the query from the method namepublic interface OrderRepository extends JpaRepository<Order, Long> {List<Order> findByCustomerId(Long customerId);}
Informative example
Custom queries when method names get awkward — use @Query:
public interface OrderRepository extends JpaRepository<Order, Long> {@Query("""select o from Order owhere o.customer.id = :customerIdand o.placedAt >= :sinceorder by o.placedAt desc""")List<Order> recentFor(@Param("customerId") Long customerId,@Param("since") Instant since);// Native SQL escape hatch@Query(value = "select count(*) from orders where total > :min", nativeQuery = true)long countAbove(@Param("min") BigDecimal min);}
Real-world use
Most real apps use Spring Data for everyday CRUD and reach for JdbcTemplate when they hit a performance ceiling (bulk inserts, complex reports, OLAP-style queries). The two coexist happily — they share the same DataSource and the same exception hierarchy.
Best practices
- Use
JdbcTemplatefor reports and bulk loads where SQL control matters. - Use Spring Data for CRUD-heavy domains where 80% of queries are obvious.
- Fetch only the columns you need — avoid
select *via projections (interface- or DTO-based). - Wrap every multi-statement workflow in
@Transactionalat the service layer.
Common mistakes
- Returning JPA entities from controllers — they trigger lazy loads outside transactions and serialise more than you intended.
- N+1 query problems — fetch relationships explicitly with
JOIN FETCHor entity graphs. - Calling repository methods in a loop where a single batched query would do.
Hands-on exercise
Try this: build a tiny BookRepository three ways — once with JdbcTemplate, once with EntityManager, once with Spring Data — backed by an H2 database. Implement findByAuthor(String) in each. Confirm the generated SQL with spring.jpa.show-sql=true and compare the line counts.