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/commentsHTTP/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 record | Repository |
|---|---|
| Less code, everything in one class | The entity is separate from the queries |
Static methods, Post.listAll() | Bean methods, commentRepository.listAll() |
| Handy for small services | Easy 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.