19. Спроектировать и реализовать REST API для сущностей "Книги" и "Авторы"
Условие задачи:
Опишите CRUD операции для сущностей, используя формат “GET /some/path 200;”.
Нужно реализовать следующие операции для сущности книги (BookEntity) с учётом таблиц: books, authors, books_authors:
- Получение всех книг
- Получение конкретной книги
- Обновление книги
- Создание книги
- Удаление книги
Спойлеры к решению
Подсказки
💡 Лучше разделить вход/выход через DTO:
💡 Для many-to-many удобно в сервисе: сохраняем
💡 Возвращай корректные статусы:
💡 Для ручной проверки добавь
BookCreateDto, BookUpdateDto, BookResponseDto.💡 Для many-to-many удобно в сервисе: сохраняем
book, затем пересоздаём связи в books_authors.💡 Возвращай корректные статусы:
201 на create, 204 на delete, 404 если книги нет.💡 Для ручной проверки добавь
.http файлы: create/get/list/update/delete.Решение
DTO:
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.List;
public class BookCreateDto {
@NotBlank
private String title;
@NotNull
private Integer publishYear;
@NotNull
private List<Long> authorIds;
// getters/setters
}
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.List;
public class BookUpdateDto {
@NotBlank
private String title;
@NotNull
private Integer publishYear;
@NotNull
private List<Long> authorIds;
// getters/setters
}
import java.util.List;
public class BookResponseDto {
private Long id;
private String title;
private Integer publishYear;
private List<Long> authorIds;
// getters/setters/constructors
}
Service (пример “в памяти”, имитируем many-to-many через отдельное хранилище связей):
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class BookService {
private final AtomicLong idSeq = new AtomicLong(0);
// books table
private final Map<Long, BookEntity> books = new ConcurrentHashMap<>();
// books_authors table: book_id -> authorIds
private final Map<Long, List<Long>> bookAuthors = new ConcurrentHashMap<>();
public List<BookResponseDto> getAll() {
return books.values().stream()
.sorted(Comparator.comparing(BookEntity::getId))
.map(this::toDto)
.toList();
}
public BookResponseDto getById(long id) {
BookEntity book = books.get(id);
if (book == null) throw new NoSuchElementException("Book not found: " + id);
return toDto(book);
}
public BookResponseDto create(BookCreateDto dto) {
long id = idSeq.incrementAndGet();
BookEntity book = new BookEntity();
book.setId(id);
book.setTitle(dto.getTitle());
book.setPublishYear(dto.getPublishYear());
books.put(id, book);
// books_authors
bookAuthors.put(id, new ArrayList<>(dto.getAuthorIds()));
return toDto(book);
}
public BookResponseDto update(long id, BookUpdateDto dto) {
BookEntity book = books.get(id);
if (book == null) throw new NoSuchElementException("Book not found: " + id);
book.setTitle(dto.getTitle());
book.setPublishYear(dto.getPublishYear());
books.put(id, book);
// пересоздаём связи
bookAuthors.put(id, new ArrayList<>(dto.getAuthorIds()));
return toDto(book);
}
public void delete(long id) {
if (books.remove(id) == null) throw new NoSuchElementException("Book not found: " + id);
bookAuthors.remove(id);
}
private BookResponseDto toDto(BookEntity book) {
BookResponseDto dto = new BookResponseDto();
dto.setId(book.getId());
dto.setTitle(book.getTitle());
dto.setPublishYear(book.getPublishYear());
dto.setAuthorIds(bookAuthors.getOrDefault(book.getId(), List.of()));
return dto;
}
}
Controller:
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.NoSuchElementException;
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService service;
public BookController(BookService service) {
this.service = service;
}
// 1) Получение всех книг
@GetMapping
public List<BookResponseDto> getAll() {
return service.getAll();
}
// 2) Получение конкретной книги
@GetMapping("/{id}")
public BookResponseDto getById(@PathVariable long id) {
return service.getById(id);
}
// 4) Создание книги
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public BookResponseDto create(@Valid @RequestBody BookCreateDto dto) {
return service.create(dto);
}
// 3) Обновление книги
@PutMapping("/{id}")
public BookResponseDto update(@PathVariable long id, @Valid @RequestBody BookUpdateDto dto) {
return service.update(id, dto);
}
// 5) Удаление книги
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable long id) {
service.delete(id);
}
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NoSuchElementException.class)
public String notFound(NoSuchElementException e) {
return e.getMessage();
}
}
.http тесты
books-create.http
POST http://localhost:8080/api/books
Content-Type: application/json
{
"title": "Clean Code",
"publishYear": 2008,
"authorIds": [1, 2]
}
books-get.http
GET http://localhost:8080/api/books/1
books-list.http
GET http://localhost:8080/api/books
books-update.http
PUT http://localhost:8080/api/books/1
Content-Type: application/json
{
"title": "Clean Code (2nd edition)",
"publishYear": 2025,
"authorIds": [2]
}
books-delete.http
DELETE http://localhost:8080/api/books/1