Spring Data JPA
Entities, repositories, relationships and derived queries.
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.
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 ...
}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.
@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));
}
}@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.
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: trueFor real projects manage the schema with Flyway or Liquibase migration scripts, and let Hibernate only validate it.
Try it yourself
Add a repository method that returns tasks created after a given Instant, and a service method that returns the count of completed tasks.
Show solution
List<Task> findByCreatedAtAfter(Instant since);
long countByDone(boolean done);
// service
public long completedCount() { return repo.countByDone(true); }