Encodera

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

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

Оглавление

Репозиторий 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/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!"}]

Второй комментарий добавлен таким же запросом от Анны. Комментарий к несуществующему посту вернёт 404 с {"error":"Пост 99 не найден"} — сработает обработчик из урока об ошибках.

Какой стиль выбрать

Активная записьРепозиторий
Меньше кода, всё в одном классеСущность отдельно от запросов
Статические методы Post.listAll()Методы бина commentRepository.listAll()
Удобно для небольших сервисовУдобно подменять в тестах

Частая ошибка

Если вернуть из ресурса саму сущность Comment, Jackson сериализует и вложенный post со всеми полями. А если у Post появится обратная связь List<Comment> comments, сериализация уйдёт в бесконечный цикл «пост — комментарий — пост» и закончится ошибкой. Отдавайте наружу записи-представления вроде CommentView, а не сущности со связями.

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

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