
Продолжаю рассказывать про свой учебный проект, который может кому-нибудь помочь в изучении Spring и Rest-сервисов. Сегодня на очереди как раз сервисы и контроллеры. Ранее я уже писал про свои основные проблемки при создании data-layer: репозиториев и сущностей. Также, пока я писал эту статью, понял, что следовало бы ещё про исключения немного поговорить, потому что они здесь в коде будут встречаться и пугать неподготовленных людей. Меня вот пугали в своё время.
Отношения One To Many в проекте Java Spring (Hibernate)
Составной первичный ключ в JPA/ Hibernatе
Обработка исключений в Spring REST приложении: @RestControllerAdvice и @ExceptionHandler
Начну с краткого описания, зачем вообще сервисы и контроллеры в приложениях нужны. Контроллер — это часть MVC-паттерна, собственно. буква C. То есть слой, который отвечает на веб-запросы и связывает две другие буковки: M с V (модель, то есть ваши данные, и представление, то есть визуальное представление данных, с которым работает пользователь). REST-controller это, грубо говоря, то же самое, только он выдаёт данные, например, в формате JSON или xml.
Сервисы являются прослойкой между слоем с данными и контроллерами. Именно в этот слой следует пихать всю бизнес-логику (то есть дополнительные вычисления и преобразования данных, которые мы достаём из репозиториев). Таким образом нам не нужно писать бизнес-логику в контроллере — теперь она изолирована в отдельном классе. Сервисы и контроллеры тогда получаются более компактными классами, их легче редактировать, заменять и вообще понимать.
Пока я учился, пересмотрел много примеров и несколько лекций, и далеко не во всех уровень сервисов существует. Если у вас нет какой-то сложной бизнес-логики, и вам нужно просто доставать/записывать данные в базу, то нет ничего зазорного в том, чтобы сервисы вообще не использовать. Но я даже в своём учебном проекте решил всё сделать по уму, поэтому начну с такого полноценного варианта.
Сервисы @Service
Так как в статье про Hibernate и то. как связывать между собой сущности, я говорил о библиотеке релизов (схема Артист — Альбом — Релиз), то и тут продолжу работать с ней же.
Итак, нам нужно три сервиса, каждый из которых будет взаимодействовать с репозиторием. Начнём с артистов:
@Service
public class ArtistService {
}
Сервисы аннотируем @Service.
Чтобы взаимодействовать с репозиторием, нам нужно его добавить в список полей класса и инициализировать в конструкторе:
private final ArtistRepository repository;
public ArtistService(ArtistRepository repository) {
this.repository = repository;
}
Так как мой проект тренировочный, я хотел написать как можно больше методов, а лишь потом разбираться, нужны ли они вообще. Но самыми ходовыми однозначно остаются методы, которые соответствуют четырём операциям CRUD. Мы должны уметь доставать из базы запись, доставать из базы все записи, создавать запись, обновлять запись и удалять её.
CRUD: реализация Read в сервисах
Начну с чтения. Для этого я создал методы getAll() и getArtistById(long id). Так как первый метод просто достаёт из базы всех артистов, то возвращает этот метод список с артистами (List<Artist>), а второй метод ищет лишь одного подходящего артиста по его id (первичный ключ в табличке), поэтому его return-type просто Artist.
public List<Artist> getAll() {
return repository.findAll();
}
public Artist getArtistById(long id) {
if (repository.findById(id).isPresent())
return repository.findById(id).get();
else
throw new NoSuchMusicEndpointException("no such artist");
}
Метод findById (да и вообще методы findBy) возвращают Optional<Artist> тип данных, поэтому внутри может скрываться null. Вдруг пользователь попросит у нас показать ему артиста и id, которого в базе нет. И чтобы всё не рухнуло, мы проверяем, а есть ли там что? Если есть, то возвращаем это значение с помощью метода get(), а если нет — выкидываем исключение. С ним справляется ExceptionHandler, о котором я говорил в прошлой статье. Можно и не выкидывать исключение, а просто выводить что-нибудь в консоль — в учебном проекте это не принципиально.
К моему методу у меня лишь одно замечание: его можно сделать короче. есть великолепный метод orElseThrow(), который позволит нам избавиться от условного блока в коде.
public Artist getArtistById(long id) {
return repository.findById(id)
.orElseThrow( () -> new NoSuchMusicEndpointException("no such artist"));
}
Никто вам также не мешает создавать более интересные вариации для поиска. Может, вам нужно искать артиста по имени? Пожалуйста, так тоже можно сделать:
public Artist getArtistByName(String name) {
return repository.findArtistByName(name);
}
Правда, по умолчанию в репозитории такого метода нет. Если findById есть везде, потому что логично считать, что у репозиториев есть идентификатор, то поле name хранит весьма уникальную характеристику нашего артиста. Поэтому интерфейс ArtistRepository дополняется прототипом метода.
public interface ArtistRepository extends JpaRepository<Artist,Long> {
public Artist findArtistByName(String name);
}
CRUD: реализация Create в сервисах
С созданием всё легко и просто. Мы просто вызываем метод save в репозитории и передаём туда объект типа Artist.
public Artist saveArtist(Artist artist) {
return repository.save(artist);
}
CRUD: реализация Update в сервисах
Если мы хотим отредактировать существующую запись, то мы берём её из репозитория, записываем в неё все обновлённые данные и сохраняем обратно в репозитирий.
public Artist editArtist(Artist updatedArtist, long id) {
return repository.findById(id)
.map(artist -> {
artist.setBio(updatedArtist.getBio());
artist.setName(updatedArtist.getName());
artist.setCountry(updatedArtist.getCountry());
return repository.save(updatedArtist);
})
.orElseThrow( () -> new NoSuchMusicEndpointException("Can't update artist: no such artist"));
}
Есть ещё другой сценарий обновления: если у нас не «полноценный» обновлённый объект (то есть не такой, где все поля хранят значения), а только эти самые обновлённые поля (то есть остальные поля, которые мы менять не хотели, нулевые — или null), Но об этом я напишу в отдельной статье: для этого нужно сравнивать Put и Patch, а я даже до этих слов-то не добрался. Так что будем считать, что я немного заглянул вперёд, но оставил всё самое интересное на потом.
CRUD: реализация Delete в сервисах
Удаление получается таким же быстрым, как и сохранение.
public void deleteArtist(long id) {
repository.deleteById(id);
}
Что получилось? Классы сервисов целиком
И на этом я с сервисами остановлюсь. Только напоследок напишу все три класса полностью. Начну с сервиса Артистов, о котором я и говорил всё это время.
package com.winylka.business.services;
import com.winylka.Exceptions.NoSuchMusicEndpointException;
import com.winylka.data.entities.Artist;
import com.winylka.data.repositories.ArtistRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ArtistService {
private final ArtistRepository repository;
public ArtistService(ArtistRepository repository) {
this.repository = repository;
}
public List<Artist> getAll() {
return repository.findAll();
}
public Artist getArtistById(long id) {
return repository.findById(id)
.orElseThrow(() -> new NoSuchMusicEndpointException("no such artist"));
}
public Artist getArtistByName(String name) {
return repository.findArtistByName(name);
}
public Artist saveArtist(Artist artist) {
return repository.save(artist);
}
public Artist editArtist(Artist updatedArtist, long id) {
return repository.findById(id)
.map(artist -> {
artist.setBio(updatedArtist.getBio());
artist.setName(updatedArtist.getName());
artist.setCountry(updatedArtist.getCountry());
return repository.save(updatedArtist);
})
.orElseThrow( () -> new NoSuchMusicEndpointException("Can't update artist: no such artist"));
}
public void deleteArtist(long id) {
repository.deleteById(id);
}
}
Пока никаких сюрпризов. Далее на очереди альбомы.
package com.winylka.business.services;
import com.winylka.Exceptions.NoSuchMusicEndpointException;
import com.winylka.data.entities.Album;
import com.winylka.data.repositories.AlbumRepository;
import org.springframework.stereotype.Service;
import java.util.Calendar;
import java.util.List;
@Service
public class AlbumService {
private final AlbumRepository repository;
private final ArtistService artistService;
public AlbumService(AlbumRepository albumRepository, ArtistService artistService) {
this.repository = albumRepository;
this.artistService = artistService;
}
public List<Album> getAllAlbums() {
return repository.findAll();
}
public List<Album> getAllAlbumsByArtist(long id) {
return repository.findAlbumsByArtistArtistId(id);
}
public List<Album> getAllAlbumsByArtistName(String name) {
return getAllAlbumsByArtist(
artistService.getArtistByName(name).getArtistId());
}
public Album getAlbumById(long id) {
return repository.findById(id)
.orElseThrow( () -> new NoSuchMusicEndpointException("no such album"));
}
public List<Album> getAlbumsByYear(int year) {
return repository.findAlbumsByYear(year);
}
public List<Album> getAlbumsThisYear() {
return repository.findAlbumsByYear(
Calendar.getInstance().get(Calendar.YEAR));
}
public Album saveAlbum(Album album) {
return repository.save(album);
}
public Album editAlbum(Album editedAlbum, long id) {
return repository.findById(id)
.map(album -> {
album.setInfo(editedAlbum.getInfo());
album.setYear(editedAlbum.getYear());
album.setTitle(editedAlbum.getTitle());
return album;
})
.orElseThrow(() -> new NoSuchMusicEndpointException("Cannot update the album: no such album"));
}
public void deleteAlbum(long id) {
repository.deleteById(id);
}
}
Здесь тоже никаких сюрпризов, только я добавил немного больше гет-методов. Например, я позволяю находить альбомы по году выпуска и также альбомы, вышедшие в этом году, в методах getAlbumsByYear и getAlbumsThisYear. соответственно. И на всякий случай создал поиск альбомов не только по id артиста, но и по его имени.
Всё в том же стиле происходит и в сервисе релизов. Только тут мы можем искать релизы и по имени артиста и по имени альбома. Количество методов для чтения из базы увеличилось, но смысл всё тот же.
package com.winylka.business.services;
import com.winylka.Exceptions.NoSuchMusicEndpointException;
import com.winylka.data.entities.Album;
import com.winylka.data.entities.Release;
import com.winylka.data.repositories.OrderItemRepository;
import com.winylka.data.repositories.ReleaseRepository;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class ReleaseService {
private final ReleaseRepository repository;
private final AlbumService albumService;
private final OrderItemService orderitems;
public ReleaseService(ReleaseRepository repository, AlbumService albumService, OrderItemService orderitems) {
this.repository = repository;
this.albumService = albumService;
this.orderitems = orderitems;
}
public List<Release> getAll() {
return repository.findAll();
}
public Release getReleaseById(Long id) {
return repository.findById(id)
.orElseThrow( () -> new NoSuchMusicEndpointException("There's no such release"));
}
public List<Release> getReleasesYetToCome(Date date) {
return repository.findReleasesByReleaseDateBetween(new Date(), date);
}
public List<Release> getFreshReleases(Date date) {
return repository.findReleasesByReleaseDateBetween(date, new Date());
}
public List<Release> getReleasesByAlbumId(long id) {
return repository.findReleasesByAlbumAlbumId(id);
}
public List<Release> getReleasesByArtistId(long id) {
return albumService.getAllAlbumsByArtist(id)
.stream().map(Album::getReleases)
.flatMap(List::stream)
.collect(Collectors.toList());
}
public long getArtistIdByReleaseId(long releaseId) {
return albumService.getAlbumById(
repository.findByReleaseId(releaseId).getAlbum().getAlbumId()
).getArtist().getArtistId();
}
public List<Release> getReleasesByAlbumName(String albumName, String artistName) {
long id = albumService.getAllAlbumsByArtistName(artistName).stream()
.filter(album -> album.getTitle().equals(albumName)).findFirst().get().getAlbumId();
return getReleasesByAlbumId(id);
}
public Release save(Release release) {
return repository.save(release);
}
public Release edit(Release editedRelease, long id) {
return repository.findById(id)
.map(release -> {
release.setReleaseDate(editedRelease.getReleaseDate());
release.setImg(editedRelease.getImg());
release.setNotes(editedRelease.getNotes());
release.setPrice(editedRelease.getPrice());
return release;
})
.orElseThrow(NoSuchMusicEndpointException::new);
}
public void release(long id) {
orderitems.setStatusToProgressForAll(id);
}
public void delete(long id) {
repository.deleteById(id);
}
}
Контроллеры @RestController
С сервисами разобрались, теперь время контроллеров. Создаём класс и обозначаем его аннотацией:
@RestController
public class ArtistController {
}
Здесь мы сопоставляем http-запросы с методами сервисов. Например, чтобы получить список артистов или одного артиста, нам нужно реализовать два метода:
@GetMapping("/store/artists")
public List<Artist> all() {
return service.getAll();
}
@GetMapping("/store/artists/{id}")
public Artist one(@PathVariable long id) {
return service.getArtistById(id);
}
Называть метод можно как угодно — здесь главный адрес страницы. Он задаёт адрес, при переходе на который будет вызываться этот метод.
Для простоты я использую аннотацию @GetMapping — иногда можно встретить вариант @RequestMapping(method = RequestMethod.GET)
, но мой вариант короче и проще, правда же?
Если какой-то фрагмент адреса оказывается переменным, мы ставим его в фигурные скобки, а потом можем использовать в методе с помощью аннотации @PathVariable, как у меня происходит в методе one() с параметром id.
Если мы хотим создать нового артиста, то нужно использовать метод POST. Соответственно и аннотация изменится.
@PostMapping("/store/artists")
@ResponseStatus(HttpStatus.CREATED)
public void addArtist(@RequestBody Artist artist) {
service.saveArtist(artist);
}
А так как от Поста клиент будет ждать ответа, в аннотации @ResponseStatus мы сообщаем, каким будет ответ. HttpStatus.CREATED звучит идеально для только что созданного ресурса.
Для обновления используем @PutMapping и немного другой статус. Ничего нового мы не создаём, но оповестить о том, что всё прошло ОК, хотелось бы.
@PutMapping("/store/artists/{id}")
@ResponseStatus(HttpStatus.OK)
public void editArtists(@RequestBody Artist artist, @PathVariable long id) {
service.editArtist(artist, id);
}
Для удаления есть аннотация @DeleteMapping, и она должна возвращать сообщение, что ресурса по этому адресу больше нет — NO_CONTENT.
@DeleteMapping("/store/artists/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteArtist(@PathVariable long id){
service.deleteArtist(id);
}
One-to-Many отношения и добавление данных через контроллер
Простота контроллера ArtistController в том, что сущность Artist в базе данных не является владельцем отношений One-to-Many. Можете подробнее почитать об устройстве базы в первой статье из цикла, но я на всякий случай напомню происходящее.
Как организовано отношение Артиста и альбома в классе Artist:
@OneToMany(mappedBy = "artist",
fetch = FetchType.LAZY,
cascade = CascadeType.ALL)
List<Album> albums = new ArrayList<>();
public void addAlbum(Album album) {
albums.add(album);
album.setArtist(this);
}
public void removeAlbum(Album album) {
albums.remove(album);
album.setArtist(null);
}
А класс Album оказывается владельцем этих отношений, и там связь отображена следующим образом:
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name="ARTIST_ID", nullable = false)
@JsonIgnore
private Artist artist;
Так вот, если мы хотим добавить или удалить альбом, то нужно удостовериться, что мы его не просто добавляем в репозиторий с помощью сервиса, но и с помощью вспомогательных методов помещаем в список albums класса Artist.
Поэтому методы для удаления и создания нового альбома в контроллере AlbumController выглядят так:
@PostMapping("/store/artists/{idArtist}/albums")
@ResponseStatus(HttpStatus.CREATED)
public void addAlbum(@RequestBody Album album, @PathVariable long idArtist) {
artistService.getArtistById(idArtist).addAlbum(album);
service.saveAlbum(album);
}
@DeleteMapping({"/store/albums/{id}",
"/store/artists/{idArtist}/albums/{id}"})
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteAlbum(@PathVariable long id, @PathVariable long idArtist) {
artistService.getArtistById(idArtist).removeAlbum(service.getAlbumById(id));
service.deleteAlbum(id);
}
Остальные методы не отличаются от схожих методов в классе ArtistController, поэтому описывать их не буду, а просто предлагаю посмотреть, что же получилось в общем.
Классы рест-контроллеров целиком
Сервисы целиком я уже выше запостил, теперь черёд контроллеров. Начну с артистов, раз с ними мы дольше всех возились.
package com.winylka.restcontrollers;
/* list of import */
@RestController
public class ArtistController {
private final ArtistService service;
public ArtistController(ArtistService service) {
this.service = service;
}
@GetMapping("/store/artists")
public List<Artist> all() {
return service.getAll();
}
@GetMapping("/store/artists/{id}")
public Artist one(@PathVariable long id) {
return service.getArtistById(id);
}
@PostMapping("/store/artists")
@ResponseStatus(HttpStatus.CREATED)
public void addArtist(@RequestBody Artist artist) {
service.saveArtist(artist);
}
@PutMapping("/store/artists/{id}")
@ResponseStatus(HttpStatus.OK)
public void editArtists(@RequestBody Artist artist, @PathVariable long id) {
service.editArtist(artist, id);
}
@DeleteMapping("/store/artists/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteArtist(@PathVariable long id){
service.deleteArtist(id);
}
}
Далее на очереди альбомы.
package com.winylka.restcontrollers;
/* list of imports */
@RestController
public class AlbumController {
private final AlbumService service;
private final ArtistService artistService;
public AlbumController(AlbumService service, ArtistService artistService) {
this.service = service;
this.artistService = artistService;
}
@GetMapping("/store/albums")
public List<Album> all() {
return service.getAllAlbums();
}
@GetMapping({"/store/albums/{id}",
"/store/artists/{idArtist}/albums/{id}"})
public Album one(@PathVariable long id) {
return service.getAlbumById(id);
}
@GetMapping("/store/artists/{idArtist}/albums")
public List<Album> allByArtist(@PathVariable long idArtist) {
return service.getAllAlbumsByArtist(idArtist);
}
@PostMapping("/store/artists/{idArtist}/albums")
@ResponseStatus(HttpStatus.CREATED)
public void addAlbum(@RequestBody Album album, @PathVariable long idArtist) {
artistService.getArtistById(idArtist).addAlbum(album);
service.saveAlbum(album);
}
@PutMapping("/store/artists/{idArtist}/albums/{id}")
@ResponseStatus(HttpStatus.OK)
public void editAlbum(@PathVariable long id, @RequestBody Album album) {
service.editAlbum(album, id);
}
@DeleteMapping("/store/artists/{idArtist}/albums/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteAlbum(@PathVariable long id, @PathVariable long idArtist) {
artistService.getArtistById(idArtist).removeAlbum(service.getAlbumById(id));
service.deleteAlbum(id);
}
}
Обратите внимание, что в основном все эндпойнты вложенные. На примере метода one():
/store/artists/{idArtist}/albums/{id}
Мне такая стратегия показалась более логичной: у каждого артиста есть альбомы и т.д. Но минус такой вложенной структуры в нейминге в том, что адреса становятся всё длиннее, и иногда в адресе мы указываем идентификатор idArtist, который потом не используем в функции. Чтобы проиллюстрировать другую стратегию — без вложенных эндпойнтов, я добавил вариант «/store/albums/{id}».
Мне также было интересно, можно ли вызывать один метод из двух разных мест. Оказалось, что хоть из трёх. Тогда все адреса перечисляются через запятую в фигурных скобках:
@GetMapping( {«/store/albums/{id}», «/store/artists/{idArtist}/albums/{id}»} )
И остались у нас только релизы.
package com.winylka.restcontrollers;
/* list of importa */
@RestController
public class ReleaseController {
private final ReleaseService service;
private final AlbumService albumService;
public ReleaseController(ReleaseService service, AlbumService albumService) {
this.service = service;
this.albumService = albumService;
}
@GetMapping("/store")
public List<Release> all() {
return service.getAll();
}
@GetMapping({"/store/{id}",
"/store/artists/{artistId}/albums/{albumId}/releases/{id}"})
public Release one(@PathVariable long id) {
return service.getReleaseById(id);
}
@GetMapping("/store/artists/{artistId}/releases")
public List<Release> allByArtist(@PathVariable long artistId) {
return service.getReleasesByArtistId(artistId);
}
@GetMapping("/store/artists/{artistId}/albums/{albumId}/releases")
public List<Release> allByAlbum(@PathVariable long albumId){
return service.getReleasesByAlbumId(albumId);
}
@PostMapping("/store/artists/{artistId}/albums/{albumId}/releases")
@ResponseStatus(HttpStatus.CREATED)
public void addRelease(@RequestBody Release release, @PathVariable long albumId) {
release.setAlbum(albumService.getAlbumById(albumId));
service.save(release);
}
@PutMapping("/store/artists/{artistId}/albums/{albumId}/releases/{id}/release")
@ResponseStatus(HttpStatus.OK)
public void releaseIt(@PathVariable long id) {
service.release(id);
}
@PutMapping("/store/artists/{artistId}/albums/{albumId}/releases/{id}")
@ResponseStatus(HttpStatus.OK)
public void editRelease(@PathVariable long id, @RequestBody Release release) {
service.edit(release, id);
}
@DeleteMapping("/store/artists/{artistId}/albums/{albumId}/releases/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteRelease(@PathVariable long id) {
service.delete(id);
}
}
Пример контроллеров без использования service-layer
Я выше говорил, что прослойку с сервисами можно опустить. В примере, описанном выше, она на самом-то деле не особо нужна. За кулисами не происходит никаких сложных вычислений, я не перегоняю объекты из базы в DTO (Domain Transfer Object). В общем, в этом примере я добавил слой сервисов лишь в учебных целях.
Сейчас же я предлагаю рассмотреть, как можно было совместить пары классов на примере ArtistService и ArtistController. А сделать это легко: вместо вызовов методов в сервисе, которые обращаются к репозиторию, мы будем обращаться к нему напрямую из контроллера.
package com.winylka.restcontrollers;
/* list of imports */
@RestController
public class ArtistController {
private final ArtistRepository repository;
public ArtistController(ArtistRepository repository) {
this.repository = repository;
}
@GetMapping("/store/artists")
public List<Artist> all() {
return repository.findAll();
}
@GetMapping("/store/artists/{id}")
public Artist one(@PathVariable long id) {
return repository.findById(id)
.orElseThrow(() -> new NoSuchMusicEndpointException("no such artist"));
}
@PostMapping("/store/artists")
@ResponseStatus(HttpStatus.CREATED)
public void addArtist(@RequestBody Artist artist) {
repository.save(artist);
}
@PutMapping("/store/artists/{id}")
@ResponseStatus(HttpStatus.OK)
public void editArtists(@RequestBody Artist updatedArtist, @PathVariable long id) {
repository.findById(id)
.map(artist -> {
artist.setBio(updatedArtist.getBio());
artist.setName(updatedArtist.getName());
artist.setCountry(updatedArtist.getCountry());
return repository.save(artist);
})
.orElseThrow( () -> new NoSuchMusicEndpointException("Can't update artist: no such artist"));
}
@DeleteMapping("/store/artists/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteArtist(@PathVariable long id){
repository.deleteById(id);
}
}
Такой вариант экономит время, если вы не планируете потом расширять приложение и добавлять сложную бизнес-логику. Иначе такой подход может, наоборот, привести к необходимости рефакторинга в будущем и подкинуть немного работы.
Разбор ошибки No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor
Напоследок хотелось бы ещё коротенько рассказать об ошибке, с которой я некоторое время промучился.
Сообщение об ошибке: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
Далее следовала длинная цепочка вызовов:
(through reference chain: com.winylka.data.entities.Artist$HibernateProxy$sJ0rdNgd[«hibernateLazyInitializer»])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77) ~[jackson-databind-2.13.2.2.jar:2.13.2.2]
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1300) ~[jackson-databind-2.13.2.2.jar:2.13.2.2]
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400) ~[jackson-databind-2.13.2.2.jar:2.13.2.2]
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:46) ~[jackson-databind-2.13.2.2.jar:2.13.2.2]
[… ещё много-много всего … ]
at java.base/java.lang.Thread.run(Thread.java:832) ~[na:na]
Решение оказалось очень простым. В репозиториях существуют метод getOne(id) и соблазняет тем, что возвращает сразу объект нужного типа. А есть метод findById(id), который возвращают Optional<>.
Первый метод ленивый и возвращает прокси объекта, а непосредственно вызова к базе не производит. Второй же метод более «активный», поэтому в контроллерах или сервисах следует использовать его. Подробнее о этих двух методах написано в этой статье.
Уфф… это была длинная статья, поэтому на сегодня всё. А в следующий раз распишу, как сделать эти rest-контроллеры по-настоящему restными. Впереди hateoas.
2 Comments