Spring Data JPA
Цель урока
Сделать полноценный CRUD для задач с правильными HTTP-статусами (201/204/404), обработкой ошибок, и подготовить Task к подключению JPA.
Теория · для собеса
1 JPA vs JDBC ▸
| JDBC | JPA |
|---|---|
| Пишешь SQL вручную | Пишешь методы, SQL генерирует Hibernate |
| RowMapper для каждого запроса | findById(42) → Optional<Task> |
| Сам управляешь Connection | Сам управляешь EntityManager (через @Transactional) |
| Сырой SQL для сложных запросов | JPQL/HQL или Native Query |
2 Что такое JPA ▸
JPA (Jakarta Persistence API) — это спецификация (интерфейсы + аннотации).
Hibernate — самая популярная реализация JPA. Spring Boot по умолчанию использует именно её.
3 Что такое Entity ▸
Entity — это Java-класс, который JPA маппит на таблицу БД.
@Entity // это JPA-сущность
@Table(name = "tasks") // имя таблицы
public class Task {
@Id // PRIMARY KEY
@GeneratedValue(strategy = GenerationType.IDENTITY) // SERIAL в PostgreSQL
private Long id;
@Column(nullable = false, length = 200) // VARCHAR(200) NOT NULL
private String title;
// ...
}
4
Что делает JpaRepository
▸
public interface TaskRepository extends JpaRepository {
// УЖЕ доступны из коробки:
// findById(Long id) → Optional
// findAll() → List
// save(Task) → Task
// deleteById(Long id)
// count() → long
// existsById(Long id) → boolean
// ...и ещё ~30 методов
}
5
@Transactional — зачем
▸
JPA работает через EntityManager, который привязан к транзакции. Без @Transactional:
findByIdработает (транзакция создаётся автоматически для read)saveброситTransactionRequiredException
Решение: на сервисных методах, которые пишут в БД, ставь @Transactional.
⚠️ Важно: @Transactional работает через AOP-прокси. Вызов из того же класса НЕ активирует транзакцию (урок 14, Spilca объясняет это).
---
Практика: улучшаем CRUD
-
1Зависимости
dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") runtimeOnly("org.postgresql:postgresql") }⚠️ Убираем
spring-boot-starter-jdbc— он не нужен, JPA приносит свой DataSource. -
2
model/Task.java(теперь JPA-Entity)package com.taskflow.model; import jakarta.persistence.*; import java.time.Instant; import java.util.Objects; @Entity @Table(name = "tasks") public class Task { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 200) private String title; @Column(columnDefinition = "TEXT") private String description; @Column(nullable = false) private boolean done = false; @Column(name = "created_at", nullable = false, updatable = false) private Instant createdAt; @Column(name = "updated_at", nullable = false) private Instant updatedAt; public Task() {} public Task(String title, String description) { this.title = title; this.description = description; } @PrePersist void onCreate() { Instant now = Instant.now(); this.createdAt = now; this.updatedAt = now; } @PreUpdate void onUpdate() { this.updatedAt = Instant.now(); } // Getters / Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public boolean isDone() { return done; } public void setDone(boolean done) { this.done = done; } public Instant getCreatedAt() { return createdAt; } public Instant getUpdatedAt() { return updatedAt; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Task task)) return false; return Objects.equals(id, task.id); } @Override public int hashCode() { return Objects.hash(id); } }Ключевые моменты:
@PrePersist/@PreUpdate— JPA-колбэки, вызываются перед INSERT/UPDATE- equals/hashCode по
id(теперь id всегда есть после persist) updatable = falseнаcreatedAt— нельзя изменить после созданияprivateполя + геттеры/сеттеры (стандарт для JPA, не для record)
-
3
repository/TaskRepository.java(JPA-версия)package com.taskflow.repository; import com.taskflow.model.Task; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface TaskRepository extends JpaRepository{ List findByDone(boolean done); List findByTitleContainingIgnoreCase(String title); } Магия Spring Data JPA: ты пишешь имя метода → Spring парсит → генерирует SQL.
findByDone→SELECT * FROM tasks WHERE done = ?findByTitleContainingIgnoreCase→SELECT * FROM tasks WHERE LOWER(title) LIKE LOWER('%' || ? || '%')
Поддерживаемые ключевые слова:
findBy,countBy,deleteBy,And,Or,Containing,IgnoreCase,OrderBy,Top,Distinct,Between,LessThan,GreaterThan,IsNull,In,NotIn... -
4
service/TaskService.javapackage com.taskflow.service; import com.taskflow.model.Task; import com.taskflow.repository.TaskRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @Service public class TaskService { private final TaskRepository repo; public TaskService(TaskRepository repo) { this.repo = repo; } public ListfindAll() { return repo.findAll(); } public List findByDone(boolean done) { return repo.findByDone(done); } public Optional findById(Long id) { return repo.findById(id); } @Transactional public Task create(Task task) { return repo.save(task); } @Transactional public Optional update(Long id, Task task) { return repo.findById(id).map(existing -> { existing.setTitle(task.getTitle()); existing.setDescription(task.getDescription()); existing.setDone(task.isDone()); return repo.save(existing); }); } @Transactional public boolean delete(Long id) { if (!repo.existsById(id)) return false; repo.deleteById(id); return true; } } -
5
application-dev.yml— настройка JPAspring: jpa: hibernate: ddl-auto: validate # НЕ создаёт таблицы сам — Flyway уже создал show-sql: true # печатает SQL в консоль properties: hibernate.format_sql: true hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect open-in-view: false # убираем lazy-loading warning'и (см. вопрос 4) -
6Запуск
./gradlew bootRunПроверяем:
$ curl -X POST http://localhost:8080/api/tasks \ -H "Content-Type: application/json" \ -d '{"title":"Read JPA docs","description":"Understand @Entity","done":false}' # В консоли увидишь SQL: # Hibernate: insert into tasks (created_at, description, done, title, updated_at) values (?, ?, ?, ?, ?) # Hibernate: select t1_0.id, t1_0.created_at, ... from tasks t1_0 where t1_0.id=?---
Зачем это на собесе
После урока ты должен уметь ответить на:
findById сам конвертит ResultSet в Task.findById в той же транзакции не идёт в БД.mock(TaskRepository.class).5 вопросов на углубление
Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.
Разбор внутри: генерация id, batch insert и практичный выбор для Postgres.
Разбор внутри: create, update, validate и почему схему лучше вести миграциями.
Разбор внутри: автоматическое заполнение createdAt/updatedAt без ручного кода в сервисах.
Разбор внутри: ленивые связи, лишние SQL-запросы и честные границы транзакций.
Разбор внутри: persist/update/delete должны выполняться внутри управляемой транзакции.
Готов идти дальше?
Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.
🚀 Перейти к Уроку 7Сначала пройди все секции и выбери хотя бы 1 вопрос
⬅️ Назад к Уроку 5