5. Спроектировать и реализовать REST API для сущностей "Книги" и "Авторы"
Условие задачи:
Необходимо спроектировать REST API для работы с книгами и авторами.
В базе данных используются таблицы:
books
authors
books_authors
Для книг необходимо реализовать:
получение всех книг;
получение книги по
id;создание книги;
обновление книги;
удаление книги.
Также нужно описать API в формате:
GET /some/path 200;
Связь между книгами и авторами — Many-to-Many.
Спойлеры к решению
Подсказки
💡 Для книг и авторов создай отдельные JPA-сущности.
💡 Связь можно описать через
💡 В REST API лучше использовать DTO, а не возвращать JPA-сущности напрямую.
💡 При создании и обновлении книги получай авторов по переданным
💡 Для создания возвращай
💡 Если книга не найдена — возвращай
💡 Связь можно описать через
@ManyToMany и таблицу books_authors.💡 В REST API лучше использовать DTO, а не возвращать JPA-сущности напрямую.
💡 При создании и обновлении книги получай авторов по переданным
authorIds.💡 Для создания возвращай
201 Created, для удаления — 204 No Content.💡 Если книга не найдена — возвращай
404 Not Found.Решение
REST API:
GET /api/books 200;
GET /api/books/{id} 200;
POST /api/books 201;
PUT /api/books/{id} 200;
DELETE /api/books/{id} 204;
Сущность автора:
@Entity
@Table(name = "authors")
public class AuthorEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// getters / setters
}
Сущность книги:
@Entity
@Table(name = "books")
public class BookEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Integer publishYear;
@ManyToMany
@JoinTable(
name = "books_authors",
joinColumns = @JoinColumn(name = "book_id"),
inverseJoinColumns = @JoinColumn(name = "author_id")
)
private Set<AuthorEntity> authors = new HashSet<>();
// getters / setters
}
DTO для создания и обновления:
public record BookRequestDto(
@NotBlank String title,
@NotNull Integer publishYear,
@NotNull Set<Long> authorIds
) {
}
DTO ответа:
public record BookResponseDto(
Long id,
String title,
Integer publishYear,
Set<Long> authorIds
) {
}
Репозитории:
public interface BookRepository
extends JpaRepository<BookEntity, Long> {
}
public interface AuthorRepository
extends JpaRepository<AuthorEntity, Long> {
}
Сервис:
@Service
@Transactional
public class BookService {
private final BookRepository bookRepository;
private final AuthorRepository authorRepository;
public BookService(
BookRepository bookRepository,
AuthorRepository authorRepository
) {
this.bookRepository = bookRepository;
this.authorRepository = authorRepository;
}
@Transactional(readOnly = true)
public List<BookResponseDto> getAll() {
return bookRepository.findAll()
.stream()
.map(this::toResponse)
.toList();
}
@Transactional(readOnly = true)
public BookResponseDto getById(long id) {
return toResponse(findBook(id));
}
public BookResponseDto create(BookRequestDto request) {
BookEntity book = new BookEntity();
book.setTitle(request.title());
book.setPublishYear(request.publishYear());
book.setAuthors(findAuthors(request.authorIds()));
return toResponse(bookRepository.save(book));
}
public BookResponseDto update(
long id,
BookRequestDto request
) {
BookEntity book = findBook(id);
book.setTitle(request.title());
book.setPublishYear(request.publishYear());
book.setAuthors(findAuthors(request.authorIds()));
return toResponse(book);
}
public void delete(long id) {
BookEntity book = findBook(id);
bookRepository.delete(book);
}
private BookEntity findBook(long id) {
return bookRepository.findById(id)
.orElseThrow(() ->
new NoSuchElementException(
"Book not found: " + id
)
);
}
private Set<AuthorEntity> findAuthors(
Set<Long> authorIds
) {
List<AuthorEntity> authors =
authorRepository.findAllById(authorIds);
if (authors.size() != authorIds.size()) {
throw new NoSuchElementException(
"One or more authors not found"
);
}
return new HashSet<>(authors);
}
private BookResponseDto toResponse(BookEntity book) {
Set<Long> authorIds = book.getAuthors()
.stream()
.map(AuthorEntity::getId)
.collect(Collectors.toSet());
return new BookResponseDto(
book.getId(),
book.getTitle(),
book.getPublishYear(),
authorIds
);
}
}
Контроллер:
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping
public List<BookResponseDto> getAll() {
return bookService.getAll();
}
@GetMapping("/{id}")
public BookResponseDto getById(
@PathVariable long id
) {
return bookService.getById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public BookResponseDto create(
@Valid @RequestBody BookRequestDto request
) {
return bookService.create(request);
}
@PutMapping("/{id}")
public BookResponseDto update(
@PathVariable long id,
@Valid @RequestBody BookRequestDto request
) {
return bookService.update(id, request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(
@PathVariable long id
) {
bookService.delete(id);
}
}
Обработка 404:
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(NoSuchElementException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleNotFound(
NoSuchElementException exception
) {
return exception.getMessage();
}
}
При создании и обновлении книги список authorIds преобразуется в набор сущностей AuthorEntity, а Hibernate самостоятельно поддерживает записи в таблице books_authors.
Для операций, изменяющих книгу и её связи с авторами, используется одна транзакция.