Репозитории
В прошлой главе мы сами написали PostRepository с картой внутри. Spring Data JPA делает такие классы ненужными: достаточно объявить интерфейс, а реализацию с SQL-запросами Spring создаст при запуске. Методы для сохранения, поиска и удаления в нём уже есть.
Репозиторий
Замените содержимое PostRepository.java:
// src/main/java/com/example/blog/PostRepository.java
package com.example.blog;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository extends JpaRepository<Post, Long> {
}JpaRepository<Post, Long> значит: репозиторий для сущности Post с ключом типа Long. Аннотация @Repository не нужна — Spring находит такие интерфейсы сам.
Готовые методы
| Метод | Что делает |
|---|---|
save(post) | Вставляет новую строку или обновляет существующую |
findById(id) | Ищет по ключу, возвращает Optional<Post> |
findAll() | Все строки таблицы |
existsById(id) | Есть ли строка с таким ключом |
count() | Количество строк |
delete(post), deleteById(id) | Удаление |
Ответ API
Сущность не стоит отдавать клиенту напрямую: в ней могут появиться служебные поля и связи. Для ответа заведём запись:
// src/main/java/com/example/blog/PostResponse.java
package com.example.blog;
import java.time.Instant;
public record PostResponse(Long id, String title, String body, String author, Instant createdAt) {
public static PostResponse from(Post post) {
return new PostResponse(post.getId(), post.getTitle(), post.getBody(),
post.getAuthor(), post.getCreatedAt());
}
}Статический метод from превращает сущность в ответ. JSON для клиента не изменится — поля те же, что были у прежней записи Post.
Сервис на базе данных
// src/main/java/com/example/blog/PostService.java
@Service
public class PostService {
private final PostRepository posts;
private final Clock clock;
public PostService(PostRepository posts, Clock clock) {
this.posts = posts;
this.clock = clock;
}
public List<PostResponse> findAll() {
return posts.findAll().stream().map(PostResponse::from).toList();
}
public PostResponse findById(Long id) {
return PostResponse.from(getPost(id));
}
public PostResponse create(NewPostRequest request) {
Post post = new Post(request.title(), request.body(), request.author(), Instant.now(clock));
return PostResponse.from(posts.save(post));
}
public PostResponse update(Long id, NewPostRequest request) {
Post post = getPost(id);
post.setTitle(request.title());
post.setBody(request.body());
return PostResponse.from(posts.save(post));
}
public void delete(Long id) {
posts.delete(getPost(id));
}
private Post getPost(Long id) {
return posts.findById(id).orElseThrow(() -> new PostNotFoundException(id));
}
}В контроллере замените тип Post на PostResponse в возвращаемых значениях. Фильтр по автору вернём в следующем уроке, а пока метод list вызывает findAll() без параметра.
Проверка
curl -i -X POST http://localhost:8080/api/posts \
-H "Content-Type: application/json" \
-d '{"title":"Привет, Spring","body":"Первая статья","author":"anna"}'HTTP/1.1 201
Location: /api/posts/1
{"id":1,"title":"Привет, Spring","body":"Первая статья","author":"anna","createdAt":"2026-10-04T09:37:51.134416400Z"}Номер 1 выдала база. Перезапустите приложение и выполните curl http://localhost:8080/api/posts — статья на месте, теперь она хранится в файле data/blog.mv.db. В журнале видно, какой SQL выполнил Hibernate:
Hibernate: insert into posts (author,body,created_at,title,id) values (?,?,?,?,default)Частая ошибка
findById возвращает Optional, а не сам объект. Вызов posts.findById(id).get() для несуществующего номера выбросит NoSuchElementException: No value present, и клиент получит 500. Используйте orElseThrow со своим исключением, как в методе getPost, — тогда обработчик ошибок вернёт понятный 404.