Статья адресуется тебе, бредущему во мраке ночи. Тебе, чей путь освещают лишь одинокие светлячки. В общем, программисту, изучающему Spring Boot и отношение ManyToMany в Hibernate.

Делал тестовое задание на отвлеченную музыкальную тему: написать сервис для хранения данных о музыкантах, песнях, альбомах, используя Java, Spring, Hibernate. Частью задания было создание классов «Композиции» и «Исполнители». Композицию могут исполнять многие исполнители, и исполнитель может исполнять многие композиции. Типичное bidirectional отношение ManyToMany.

На курсах Khasang рассказывали, как избежать зацикливания rest запросов на Spring с помощью DTO класса, но Spring Boot это другая история. На русскоязычных сайтах видел ответы типа «Да это все элементарно», но без конкретных пояснений. Приведу пример решения этой задачи. Полный код размещен на гитхабе, ссылка внизу.

Сначала создадим Entity: People и SongPlayers. Геттеры и сеттеры опускаю для краткости.

@Entity
public class People {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String human;
    //несколько людей играют в одной группе
    @ManyToOne(cascade = CascadeType.ALL)
    private RockGroups rockGroups;
    //разные люди исполняют разные композиции
    @ManyToMany(mappedBy = "songInstrumentalist",fetch = FetchType.EAGER)
    private List<SongPlayers> songItems;
    public People(){}
    public People(long id, String human){
        this.id = id;
        this.human = human;
    }
//. . . . . . . . .
}

@Entity
public class SongPlayers {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String song;
    //у композиции один композитор
    private String composer;
    // и один автор стихов
    private String poet;
    //песня содержится в альбоме
    private String album;
    //и много исполнителей
    //исполнители могут исполнять разные песни
    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private List<People> songInstrumentalist;
//. . . . . . . . .
}

Затем создаем Repository интерфейсы для класса People

@Repository
public interface PeopleRepository extends JpaRepository<People, Long> {
    @Query("select h from People h where h.human=?1")
    List<People> searchByHuman(String human);
    List<People> findPeopleByHuman(String human);
}

И для класса SongPlayers

@Repository
public interface SongPlayersRepository extends JpaRepository<SongPlayers, Long> {
    List<SongPlayers> findSongPlayersBySong(String song);
    List<SongPlayers> findSongPlayersByComposer(String composer);
    List<SongPlayers> findSongPlayersByPoet(String poet);
}

Аннотация Repository расширяет аннотацию Component, что позволяет имплементированный класс делать бином и соответственно аутовайрить.

Расширение интерфейса JpaRepository позволяет производить нужные CRUD операции без дополнительного описания и другие полезные вещи.

Теперь нужно создать DTO классы для Entity People и SongPlayers. Здесь приведу только PeopleDTO что бы не загромождать статью. Геттеры и сеттеры снова опустил.

public class PeopleDTO {
    private long id;
    private String human;
    private RockGroups rockGroups;
    private List<SongPlayersDTO> songPlayersList;
    public List<PeopleDTO> getPeopleDTOList(List<People> peopleList){
        List<PeopleDTO> peopleDTOList = new ArrayList<>();
        for (People people : peopleList){
            songPlayersList = new ArrayList<>();
            PeopleDTO peopleDTO = new PeopleDTO();
            peopleDTO.setId(people.getId());
            peopleDTO.setHuman(people.getHuman());
            peopleDTO.setRockGroups(people.getRockGroups());
            for (SongPlayers songPlayers : people.getSongItems()){
                SongPlayersDTO songPlayersDTO = new SongPlayersDTO();
                songPlayersDTO.setId(songPlayers.getId());
                songPlayersDTO.setSong(songPlayers.getSong());
                songPlayersDTO.setPoet(songPlayers.getPoet());
                songPlayersDTO.setComposer(songPlayers.getComposer());
                songPlayersDTO.setAlbum(songPlayers.getAlbum());
                songPlayersList.add(songPlayersDTO);
            }
            peopleDTO.setSongPlayersList(songPlayersList);
            peopleDTOList.add(peopleDTO);
        }
        return peopleDTOList;
    }
//. . . . . . . . .
}

По аналогии создается и SongPlayersDTO класс. Выбираем нужные поля для отображения в rest — ответе.

Создадим контроллер для пипла. Внимание, следите за руками!

@RestController
@RequestMapping("/people")
public class PeopleController {
    @Autowired
    private PeopleRepository repository;
    @GetMapping("/all")
    public List<PeopleDTO> getAllPeople(){
        PeopleDTO peopleDTO = new PeopleDTO();
        return peopleDTO.getPeopleDTOList(repository.findAll());
    }
//. . . . . . .
}

Ага, скажете Вы. Так работать не будет. А где имплементация интерфейса PeopleRepository?
Будет работать, класс PeopleRepository создается на лету! Просто фантастика.

А теперь сравните количество создаваемых классов и интерфейсов для идентичного проекта на Spring и Spring Boot
классы в проекте Spring классы в Spring Boot

Желаю всем успешного программирования. Жду ваших комментариев.


Используемая литература:

  • Felipe Gutierrez Pro Spring Boot
  • Craig Walls Spring in action 5-th edition

Комментарии (11)


  1. solver
    06.12.2018 17:18

    Чтобы не писать портянки такого ужасного кода по перекладыванию Entity в Dto, посмотрите не проект
    Mapstruct
    Это не единственный вариант. Но мне больше нравится тем, что не в рантайме все это мапит, а через кодогенерацию.


    1. developerc Автор
      06.12.2018 19:22
      +1

      Благодарю за полезные ссылки. Обязательно поизучаю Mapstruct и Lombok. С другой стороны, любая задача имеет несколько решений. Вот доказательств теоремы Пифагора более двадцати, но не говорят, что это доказательство неверно, потому, что его не изучают в школьной программе.


      1. solver
        07.12.2018 00:45

        Не путайте доказательства с, извините, говнокодом)
        У вас дто напрямую связано с сущностями. Это смешение слоев приложения, что является плохой практикой и на любом нормальном ревью кода его завернут сразу.


  1. altrus
    06.12.2018 17:21

    А аннотация @JsonIgnore вашу задачу не решает более изящно?


    1. developerc Автор
      06.12.2018 19:25
      +1

      Аннотация @JsonIgnore действительно спасает от зацикливания. Но и значения поля, которое она аннотирует, не передаются в rest ответе. А они нам как раз и нужны.


  1. maxzh83
    06.12.2018 17:34

    Та магия, которой вы восхищаетесь, заложена в Spring Data. Spring Boot — всего лишь следствие. Ну и помимо MapStruct (как выше советуют), можно использовать и Lombok


  1. Golem765
    07.12.2018 00:24

    Зачем в пипл репозитории два метода которые делают одно и то же, и на первый к тому же повешена аннотация которая делает в точности то что описывает название метода (то есть спринг дата сам справится с квери)?

    Суть дто не в том чтобы это был микросервис который перегонит юзеров из сущностей в объекты, а в том что хранить уже перегнанные данные, то есть метод который называется get… и принимает список пиплов это минимально дичь, то есть вы создаёте объект чтобы создать ещё объектов, в таком случае лучше было бы сделать фабрику или статический метод

    Думаю тот кто будет использовать ваш код просто офигеет когда метод начинающийся на get потребует внутрь что либо, и вернёт список объектов своего же типа


    1. developerc Автор
      07.12.2018 16:21

      Да, действительно два метода выполняют одинаковое действие, но выполнены по-разному. Скажем так, я тренировался. Повторенье мать ученья.


  1. PqDn
    07.12.2018 00:58

    Аннотация ManyToMany к Spring Boot вообще никого отношения не имеет. К JPA да, хибернейту да, к другим ORMмкам может быть. Но Spring Boot это совсем о другом.


  1. bvn13
    07.12.2018 10:51

    Мне кажется (я точно ловил), что будет NullPointerException при получении списка песен

    people.getSongItems()
    

    Т.к. гибернейт успевает закрыть транзакцию после чтения списка people-ов, и EAGER поля остаются невычитанными из БД. Соответственно, весь метод преобразования Entity в DTO с чтением EAGER полей нужно оборачивать в @Transactional аннотацию.


    1. developerc Автор
      07.12.2018 17:25

      А это ценное замечание. Добавил @Transactional. Правда, у меня NullPointerException не появлялся что тогда, что сейчас. Но зато реже стала появляться ошибка с неправильным ключем при апдейте альбома. Сейчас прогоняю в цикле 3 раза, хотя у меня альбомов 6.