Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

The Panache repository and comments

Panache has two styles. In active record the database methods live in the entity itself — that is how we built Post. In the repository style the entity stays a simple class with fields, and the queries are collected in a separate repository bean. A repository is easy to replace in tests and feels familiar to people who know Spring Data. Both styles are equal and can live in one project; we show the second one on comments to posts.

The comment entity

// src/main/java/com/example/blog/Comment.java
package com.example.blog;
 
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
 
import java.time.LocalDateTime;
 
@Entity
public class Comment {
 
    @Id
    @GeneratedValue
    public Long id;
 
    public String author;
 
    public String text;
 
    public LocalDateTime createdAt = LocalDateTime.now();
 
    @ManyToOne(optional = false)
    public Post post;
}

The class extends nothing, so id is declared explicitly: @Id is the primary key, @GeneratedValue means the database assigns the number. @ManyToOne is a "many comments to one post" relation: the Comment table gets a post_id column.

The repository

// src/main/java/com/example/blog/CommentRepository.java
package com.example.blog;
 
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
 
import java.util.List;
 
@ApplicationScoped
public class CommentRepository implements PanacheRepository<Comment> {
 
    public List<Comment> findByPost(long postId) {
        return list("post.id", Sort.by("createdAt"), postId);
    }
 
    public long countByAuthor(String author) {
        return count("author", author);
    }
}

The PanacheRepository<Comment> interface gives the repository the same methods Post had, but not static: persist, findById, listAll, count, delete. Your own queries are ordinary methods. "post.id" is a path through the relation: comments whose post id equals the parameter.

Response data

The Comment entity refers to the whole Post. Returning it as JSON as is would be a bad idea: the post would be repeated in every comment. The response is described by a record:

// src/main/java/com/example/blog/CommentView.java
package com.example.blog;
 
public record CommentView(long id, String author, String text) {
 
    static CommentView of(Comment comment) {
        return new CommentView(comment.id, comment.author, comment.text);
    }
}
// src/main/java/com/example/blog/NewComment.java
package com.example.blog;
 
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
 
public record NewComment(@NotBlank String author, @NotBlank @Size(max = 1000) String text) {
}

The comments resource

// src/main/java/com/example/blog/CommentResource.java
package com.example.blog;
 
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import org.jboss.resteasy.reactive.ResponseStatus;
 
import java.util.List;
 
@Path("/posts/{postId}/comments")
public class CommentResource {
 
    @Inject
    CommentRepository commentRepository;
 
    @GET
    public List<CommentView> list(@PathParam("postId") long postId) {
        return commentRepository.findByPost(postId).stream()
                .map(CommentView::of)
                .toList();
    }
 
    @POST
    @Transactional
    @ResponseStatus(201)
    public CommentView add(@PathParam("postId") long postId, @Valid NewComment newComment) {
        Post post = Post.<Post>findByIdOptional(postId)
                .orElseThrow(() -> new PostNotFoundException(postId));
        Comment comment = new Comment();
        comment.author = newComment.author();
        comment.text = newComment.text();
        comment.post = post;
        commentRepository.persist(comment);
        return CommentView.of(comment);
    }
}

The repository is injected with @Inject, like any bean. @ResponseStatus(201) from Quarkus REST sets the response status without Response.

curl -i -X POST http://localhost:8080/posts/1/comments \
     -H "Content-Type: application/json" -d '{"author":"Oleg","text":"Great start!"}'
curl http://localhost:8080/posts/1/comments
HTTP/1.1 201 Created
Content-Type: application/json;charset=UTF-8

{"id":1,"author":"Oleg","text":"Great start!"}
[{"id":1,"author":"Oleg","text":"Great start!"},{"id":2,"author":"Anna","text":"Thanks!"}]

The second comment was added with the same request from Anna. A comment on a missing post returns 404 with {"error":"Post 99 not found"} — the mapper from the lesson on errors does that.

Which style to choose

Active recordRepository
Less code, everything in one classThe entity is separate from the queries
Static methods, Post.listAll()Bean methods, commentRepository.listAll()
Handy for small servicesEasy to replace in tests

Common mistake

If a resource returns the Comment entity itself, Jackson serializes the nested post with all its fields too. And if Post later gets a back reference List<Comment> comments, serialization goes into an endless "post — comment — post" loop and ends with an error. Return view records such as CommentView, not entities with relations.

Thanks for reading!

Donate to the site — we work hard for you.