Spring Boot Intermediate: Layers, Data and Testing
Spring Data JPA
In this lesson you will
- Map a class to a table with JPA annotations
- Use JpaRepository, derived queries and paging
- Configure a development database and understand schema management
Introduction
JPA (Jakarta Persistence) maps Java classes to database tables. Hibernate is the implementation Spring Boot uses by default, and Spring Data JPA removes most of the boilerplate: you declare an interface, and Spring creates the implementation. This lesson is written for Spring Boot 4.1.2 (Hibernate 7). The code was not compiled or run here.
Add the dependencies Spring Data JPA and H2 Database when generating a project. H2 is an in-memory database for development and tests. It is not meant for production data.
An entity
Java
package com.example.demo.book;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private int year;
protected Book() {} // required by JPA
public Book(String title, int year) {
this.title = title;
this.year = year;
}
public Long getId() { return id; }
public String getTitle() { return title; }
public int getYear() { return year; }
}@Entitymaps the class to a table (namedbookby default).@Idmarks the primary key, and@GeneratedValuelets the database generate it.- JPA needs a no-argument constructor (it can be protected) and a non-final class, so a record cannot be an entity. Records are still excellent for request and response data.
- This
Bookreplaces theBookrecord from the basics course. In a larger app, keep the entity inside your service layer and return a separate response record from controllers, so database details do not leak into your API. To keep the lessons short, the later examples return the entity directly.
A repository
Java
package com.example.demo.book;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book, Long> {
List<Book> findByYearGreaterThan(int year);
List<Book> findByTitleContainingIgnoreCase(String text);
}You write no implementation. JpaRepository<Book, Long> already provides save, findById, findAll, deleteById, count and more. The two extra methods are derived queries: Spring reads the method name and builds the query. findByYearGreaterThan(1900) means where year > 1900.
Using it in a service:
Java
@Service
public class BookService {
private final BookRepository repository;
public BookService(BookRepository repository) { this.repository = repository; }
public Book add(String title, int year) {
return repository.save(new Book(title, year));
}
public Book get(long id) {
return repository.findById(id).orElseThrow(() -> new BookNotFoundException(id));
}
}findById returns an Optional<Book>, which forces you to handle "not found" (the next lesson shows an exception handler for that).
Quick quiz
Why can't a Java record be a JPA entity?
Custom queries
When a method name gets too long, write the query with @Query using JPQL, which queries entities and not tables:
Java
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@Query("select b from Book b where b.year between :from and :to order by b.year")
List<Book> findBetween(@Param("from") int from, @Param("to") int to);Named parameters (:from) are bound safely, which protects against SQL injection. Never concatenate user input into query strings.
Paging and sorting
Java
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
Page<Book> findAll(Pageable pageable); // already provided by JpaRepository
Page<Book> page = repository.findAll(PageRequest.of(0, 10, Sort.by("title")));A Page includes the items, the total number of elements and pages. Always paginate lists that can grow, so one request cannot load millions of rows.
Development configuration
With H2 on the classpath and no datasource URL, Boot configures an embedded in-memory database automatically. Useful properties:
Properties
spring.jpa.show-sql=true
spring.h2.console.enabled=truespring.jpa.show-sql prints the SQL Hibernate runs, which is excellent for learning. The H2 web console is for development only and must not be exposed in production. (In Boot 4 the console lives in its own module, which the Initializr adds when you select H2.)
Schema management
spring.jpa.hibernate.ddl-auto controls what Hibernate does with the schema:
create-drop(the default for an embedded database): creates tables at start and drops them at the endupdate: tries to alter the schema, which is unsafe for productionvalidate: checks the schema matches your entitiesnone: does nothing
For real data, use migrations with a tool such as Flyway or Liquibase, which apply versioned scripts in order and keep a history.
Relationships (a glance)
@OneToMany and @ManyToOne map relationships between tables. They bring important decisions about loading (lazy or eager) and performance, covered in the advanced course.
Production database
Move to a real database such as PostgreSQL by adding its driver and setting spring.datasource.url, username and password through environment variables, never in committed files.
Try it yourself
Create the Book entity and repository, and save three books at start-up using an ApplicationRunner bean. Add findByTitleContainingIgnoreCase to a search endpoint and watch the SQL in the console.
Summary
- JPA entities map classes to tables, and Spring Data repositories generate the data access code from an interface.
- Use derived queries, @Query with named parameters and Pageable, and never concatenate user input into queries.
- Use H2 for development only, avoid ddl-auto update in production and manage schemas with migrations.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading