4. Реализовать REST API для поиска обуви с фильтрацией
Условие задачи:
Необходимо реализовать REST API с эндпоинтом:
POST /api/v1/search
Эндпоинт принимает параметры фильтрации и возвращает подходящую обувь.
Условия поиска:
сапоги — цвет красный или зелёный, цена от
5000до20000;туфли — цена меньше
10000.
Пример запроса:
{
"type": "сапоги",
"color": ["красный", "зелёный"],
"minPrice": 5000,
"maxPrice": 20000
}
Пример ответа:
[
{
"type": "сапоги",
"color": "красный",
"price": 15000,
"url": "https://example.com/shoe1"
}
]
Спойлеры к решению
Подсказки
💡 Создай сущность
💡 Для динамических фильтров удобно использовать
💡 Репозиторий можно расширить через
💡 Каждый переданный фильтр добавляй только тогда, когда он не равен
💡 Контроллер должен принимать JSON через
Shoe и DTO для запроса и ответа.💡 Для динамических фильтров удобно использовать
Specification.💡 Репозиторий можно расширить через
JpaSpecificationExecutor.💡 Каждый переданный фильтр добавляй только тогда, когда он не равен
null.💡 Контроллер должен принимать JSON через
@RequestBody.Решение
Сущность:
@Entity
@Table(name = "shoes")
public class Shoe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String type;
private String color;
private BigDecimal price;
private String url;
// getters / setters
}
DTO:
public record ShoeSearchRequest(
String type,
List<String> colors,
BigDecimal minPrice,
BigDecimal maxPrice
) {
}
public record ShoeResponse(
String type,
String color,
BigDecimal price,
String url
) {
}
Репозиторий:
public interface ShoeRepository
extends JpaRepository<Shoe, Long>,
JpaSpecificationExecutor<Shoe> {
}
Сервис:
@Service
public class ShoeService {
private final ShoeRepository shoeRepository;
public ShoeService(ShoeRepository shoeRepository) {
this.shoeRepository = shoeRepository;
}
public List<ShoeResponse> search(
ShoeSearchRequest request
) {
Specification<Shoe> specification =
Specification.where(null);
if (request.type() != null) {
specification = specification.and(
(root, query, cb) ->
cb.equal(root.get("type"), request.type())
);
}
if (request.colors() != null
&& !request.colors().isEmpty()) {
specification = specification.and(
(root, query, cb) ->
root.get("color").in(request.colors())
);
}
if (request.minPrice() != null) {
specification = specification.and(
(root, query, cb) ->
cb.greaterThanOrEqualTo(
root.get("price"),
request.minPrice()
)
);
}
if (request.maxPrice() != null) {
specification = specification.and(
(root, query, cb) ->
cb.lessThanOrEqualTo(
root.get("price"),
request.maxPrice()
)
);
}
return shoeRepository.findAll(specification)
.stream()
.map(this::toResponse)
.toList();
}
private ShoeResponse toResponse(Shoe shoe) {
return new ShoeResponse(
shoe.getType(),
shoe.getColor(),
shoe.getPrice(),
shoe.getUrl()
);
}
}
Контроллер:
@RestController
@RequestMapping("/api/v1")
public class ShoeController {
private final ShoeService shoeService;
public ShoeController(ShoeService shoeService) {
this.shoeService = shoeService;
}
@PostMapping("/search")
public List<ShoeResponse> search(
@RequestBody ShoeSearchRequest request
) {
return shoeService.search(request);
}
}
Specification позволяет динамически формировать запрос только из тех фильтров, которые действительно переданы клиентом.
Для денежных значений используется BigDecimal, а не Double, чтобы избежать ошибок представления чисел с плавающей точкой.