Learn / Frameworks / Spring Boot / Spring Data JPA

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; mappedBy marks the inverse.
  • Use FetchType.LAZY so related data loads only when accessed.
  • Watch for the N+1 problem: loading a list then touching a lazy relation per item. Fix with @EntityGraph or a join fetch query.

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
Up next · Lesson 5Configuration, Profiles and Testingapplication.yml, profiles, and unit and integration tests.