Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Репозитории

В прошлой главе мы сами написали 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.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.