Spring Boot · Lesson 4 of 5
Spring Data JPA
Entities, repositories, relationships and derived queries.
- Intermediate
- 18 min read
- 3 objectives
Before this lessonLesson 3: Dependency Injection and Layers
What you will learn
- Map an entity
- Use JpaRepository
- Define relationships
JPA (Jakarta Persistence) maps Java classes to database tables, and Spring Data JPA generates the repository implementations for you. You describe the data and the queries you want, and it writes the SQL.
An entity
import jakarta.persistence.*;
import java.time.Instant;
@Entity
@Table(name = "tasks")
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String title;
private boolean done;
private Instant createdAt = Instant.now();
protected Task() {} // required by JPA
public Task(String title) { this.title = title; }
// getters and setters ...
}A repository
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface TaskRepository extends JpaRepository<Task, Long> {
List<Task> findByDoneFalseOrderByCreatedAtDesc(); // derived query
List<Task> findByTitleContainingIgnoreCase(String text);
long countByDone(boolean done);
@Query("select t from Task t where t.title like %:q% and t.done = false")
List<Task> searchOpen(String q); // custom JPQL
}There is no implementation class. Spring creates it at startup, and you get save, findById, findAll, deleteById, count and paging for free. Method names like findByDoneFalseOrderByCreatedAtDesc are parsed into queries.
Using it in a service
@Service
public class TaskService {
private final TaskRepository repo;
public TaskService(TaskRepository repo) { this.repo = repo; }
@Transactional
public Task create(String title) {
return repo.save(new Task(title));
}
public Page<Task> page(int page, int size) {
return repo.findAll(PageRequest.of(page, size, Sort.by("createdAt").descending()));
}
public Task get(Long id) {
return repo.findById(id).orElseThrow(() -> new TaskNotFoundException(id));
}
}Relationships
@Entity
class Project {
@Id @GeneratedValue private Long id;
private String name;
@OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Task> tasks = new ArrayList<>();
}
@Entity
class Task {
@Id @GeneratedValue private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "project_id")
private Project project;
}- The side with the foreign key (
@ManyToOne) is the owning side;mappedBymarks the inverse. - Use
FetchType.LAZYso related data loads only when accessed. - Watch for the N+1 problem: loading a list then touching a lazy relation per item. Fix with
@EntityGraphor ajoin fetchquery.
Configuration
spring:
datasource:
url: jdbc:postgresql://localhost:5432/appdb
username: app
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate # never 'update' or 'create' in production
show-sql: true// Write your solution here
