Page through a product's reviews
Java · Spring Boot · beginner · modification
Adds the paged reviews list for the product page. Until now the page only showed the three top-rated reviews (`findTop3ByProductIdOrderByRatingDescCreatedAtDesc`); this adds `GET /api/products/{productId}/reviews` so shoppers can read all of them, ten at a time, newest first. The repository gains a `Page`-returning `findByProductId`, `ReviewService.productReviews` builds the `PageRequest` with a stable newest-first sort and maps the result into the shop's `PageView` envelope, and `ReviewController` validates `page` with `@Min(1)` / `@Max(1000)`, defaulting it to 1.
Shop backend on Spring Boot 3.3 (Java 21), Spring Data JPA (Hibernate 6) on PostgreSQL. Existing and unchanged: the `Review` entity (`id`, `productId`, `rating`, `body`, `authorName`, `createdAt`, a non-null `Instant`); the `reviews` table holds only published reviews, so every row is public. `ReviewView` is a record with `ReviewView.from(review)`. `PageView<T>(List<T> items, int page, int totalPages, long totalItems)` is the shop's shared list envelope. The security config already permits anonymous `GET /api/products/**`. With no `@Validated` on the controller, Spring MVC 6.1's built-in method validation answers a `@Min`/`@Max` violation on a `@RequestParam` with 400, and a non-numeric `page` is a 400 type mismatch. A `Page`-returning Spring Data finder applies the `Pageable`'s offset, size and sort in the query and computes the totals (with a count query when needed). `reviews` has an index on `(product_id, created_at, id)`. A product has at most a few thousand reviews.
Requirements
- `GET /api/products/{productId}/reviews?page=N` returns one page of the product's reviews as a `PageView<ReviewView>`: newest first by `createdAt` (ties broken by `id`, newest first), 10 reviews per page.
- Page numbers start at 1, like every other list endpoint in the shop: the storefront's pager sends `page=1` for the first page, and `page` defaults to 1 when it is missing. `page=1` holds the 10 newest reviews, `page=2` the next 10, and so on. The response's `page` echoes the requested page number, and `totalPages` / `totalItems` count all of the product's reviews.
- `page` must be between 1 and 1000; anything else, or a non-number, is a 400. A page past the last one returns an empty `items` list with the same totals. A product with no reviews (including an unknown product id) gets an empty list with `totalPages` 0 and `totalItems` 0.
Files touched
- src/main/java/com/acme/shop/reviews/ReviewRepository.java
- src/main/java/com/acme/shop/reviews/ReviewService.java
- src/main/java/com/acme/shop/reviews/ReviewController.java
--- src/main/java/com/acme/shop/reviews/ReviewRepository.java
package com.acme.shop.reviews;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
List<Review> findTop3ByProductIdOrderByRatingDescCreatedAtDesc(Long productId);
+
+ Page<Review> findByProductId(Long productId, Pageable pageable);
}
--- src/main/java/com/acme/shop/reviews/ReviewService.java
+package com.acme.shop.reviews;
+
+import com.acme.shop.web.PageView;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+
+@Service
+public class ReviewService {
+
+ static final int PAGE_SIZE = 10;
+
+ private static final Sort NEWEST_FIRST = Sort.by(Sort.Order.desc("createdAt"), Sort.Order.desc("id"));