Репозиторий Panache и комментарии
У Panache два стиля. В активной записи методы работы с базой живут в самой сущности — так мы сделали Post. В стиле репозитория (repository) сущность остаётся простым классом с полями, а запросы собраны в отдельном бине-репозитории. Репозиторий удобно подменять в тестах и легче понять тем, кто знаком с Spring Data. Оба стиля равноправны и могут жить в одном проекте; покажем второй на комментариях к постам.
Сущность комментария
// 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;
}Класс ни от чего не наследуется, поэтому id объявлен явно: @Id — первичный ключ, @GeneratedValue — номер выдаёт база. @ManyToOne — связь «много комментариев к одному посту»: в таблице Comment появится столбец post_id.
Репозиторий
// 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);
}
}Интерфейс PanacheRepository<Comment> даёт репозиторию те же методы, что были у Post, но уже не статические: persist, findById, listAll, count, delete. Свои запросы добавляются обычными методами. "post.id" — путь через связь: комментарии, у которых id поста равен параметру.
Данные для ответа
Сущность Comment ссылается на весь Post. Отдавать её в JSON целиком не стоит: в каждом комментарии повторится пост. Ответ описываем записью:
// 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) {
}Ресурс комментариев
// 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);
}
}Репозиторий внедряется через @Inject, как любой бин. @ResponseStatus(201) из Quarkus REST задаёт статус ответа без 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!"}]
Второй комментарий добавлен таким же запросом от Анны. Комментарий к несуществующему посту вернёт 404 с {"error":"Пост 99 не найден"} — сработает обработчик из урока об ошибках.
Какой стиль выбрать
| Активная запись | Репозиторий |
|---|---|
| Меньше кода, всё в одном классе | Сущность отдельно от запросов |
Статические методы Post.listAll() | Методы бина commentRepository.listAll() |
| Удобно для небольших сервисов | Удобно подменять в тестах |
Частая ошибка
Если вернуть из ресурса саму сущность Comment, Jackson сериализует и вложенный post со всеми полями. А если у Post появится обратная связь List<Comment> comments, сериализация уйдёт в бесконечный цикл «пост — комментарий — пост» и закончится ошибкой. Отдавайте наружу записи-представления вроде CommentView, а не сущности со связями.