Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Repositories

In the previous chapter we wrote PostRepository ourselves, with a map inside. Spring Data JPA makes such classes unnecessary: you declare an interface, and Spring creates the implementation with SQL queries at startup. Methods for saving, finding and deleting are already there.

The repository

Replace the content of 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> means: a repository for the Post entity with a key of type Long. No @Repository annotation is needed — Spring finds such interfaces itself.

Ready-made methods

MethodWhat it does
save(post)Inserts a new row or updates an existing one
findById(id)Looks up by key, returns Optional<Post>
findAll()All rows of the table
existsById(id)Whether a row with this key exists
count()The number of rows
delete(post), deleteById(id)Deleting

The API response

An entity should not go to the client directly: it may later gain internal fields and relationships. Let's create a record for the response:

// 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());
    }
}

The static from method turns an entity into a response. The JSON for the client stays the same — the fields match the former Post record.

The service on the database

// 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));
    }
}

In the controller, change the return types from Post to PostResponse. The author filter comes back in the next lesson; for now the list method calls findAll() without a parameter.

Checking it

curl -i -X POST http://localhost:8080/api/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"Hello, Spring","body":"First post","author":"anna"}'
HTTP/1.1 201
Location: /api/posts/1
 
{"id":1,"title":"Hello, Spring","body":"First post","author":"anna","createdAt":"2026-10-04T09:37:51.134416400Z"}

The id 1 came from the database. Restart the application and run curl http://localhost:8080/api/posts — the post is still there, now stored in the data/blog.mv.db file. The log shows the SQL Hibernate ran:

Hibernate: insert into posts (author,body,created_at,title,id) values (?,?,?,?,default)

Common mistake

findById returns an Optional, not the object itself. Calling posts.findById(id).get() for a missing id throws NoSuchElementException: No value present, and the client gets a 500. Use orElseThrow with your own exception, as in the getPost method — then the error handler returns a clear 404.

Thanks for reading!

Donate to the site — we work hard for you.