Реализовать CRUD для сущностей "Книги" и "Авторы"

19. Спроектировать и реализовать REST API для сущностей "Книги" и "Авторы"

Условие задачи:

Опишите CRUD операции для сущностей, используя формат “GET /some/path 200;”.
Нужно реализовать следующие операции для сущности книги (BookEntity) с учётом таблиц: books, authors, books_authors:

  1. Получение всех книг
  2. Получение конкретной книги
  3. Обновление книги
  4. Создание книги
  5. Удаление книги
Спойлеры к решению
Подсказки
💡 Лучше разделить вход/выход через DTO: 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