Как правильно спроектировать RestController для Spring REST API с помощью JPA? - PullRequest
0 голосов
/ 26 апреля 2019

Я не знаю, как правильно спроектировать архитектуру для серверной части веб-приложения базы данных фильмов.

У меня есть объект кино, в котором есть список объектов жанра и карта актеров с их ролью в фильме:

// ...

@Data
@Entity
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Movie {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String director;
    private Date releaseDate;
    private Long posterId;

    @ManyToMany
    @JoinTable(
            name = "MOVIE_GENRES",
            joinColumns = @JoinColumn(name = "MOVIE_ID"),
            inverseJoinColumns = @JoinColumn(name = "GENRE_ID"))
    private Set<Genre> genres = new HashSet<>();

    // TODO how to rename value column (CAST -> ACTOR_ID)
    @OneToMany
    @MapKeyColumn(name = "ACTOR_ROLE")
    private Map<String, Actor> cast = new HashMap<>();

    // ...
}

У меня также есть REST-контроллер для фильмов:

@RestController
public class MovieController {

    private MovieRepository repository;

    public MovieController(MovieRepository repository) {
        this.repository = repository;
    }

    @GetMapping("/api/movies")
    public List<Movie> all() {
        return repository.findAll();
    }

    @PostMapping("/api/movies")
    Movie newMovie(@RequestBody Movie newMovie) {
        return repository.save(newMovie);
    }

    @GetMapping("/api/movies/{id}")
    Movie one(@PathVariable Long id) {
        return repository.findById(id)
                .orElseThrow(() -> new MovieNotFoundException(id));
    }

    @PutMapping("/api/movies/{id}")
    Movie replaceMovie(@RequestBody Movie newMovie, @PathVariable Long id) {
        return repository.findById(id)
                .map(movie -> {
                    movie.setTitle(newMovie.getTitle());
                    movie.setDirector(newMovie.getDirector());
                    movie.setReleaseDate(newMovie.getReleaseDate());
                    movie.setGenres(newMovie.getGenres());
                    movie.setCast(newMovie.getCast());
                    return repository.save(movie);
                })
                .orElseGet(() -> {
                    newMovie.setId(id);
                    return repository.save(newMovie);
                });
    }

    @DeleteMapping("/api/movies/{id}")
    void deleteMovie(@PathVariable Long id) {
        repository.deleteById(id);
    }
}

Вот как это выглядит, когда я звоню /api/movies. Я получаю информацию обо всех жанрах и ролях для каждого фильма. Это нормально? Мне даже не нужна вся эта информация при получении списка всех фильмов.

Если я следую принципам REST, не должен ли я получить актерский состав через /api/movies/{id}/cast? Я знаю, как добавить еще один @RestMapping, который возвращает только приведение, но это не меняет того факта, что приведение будет по-прежнему включаться в каждый вызов /api/movies.

{
    "id": 1,
    "title": "The Matrix",
    "director": null,
    "releaseDate": null,
    "posterId": 1,
    "genres": [
        {
            "id": 4,
            "name": "Science Fiction"
        },
        {
            "id": 1,
            "name": "Action"
        }
    ],
    "cast": {
        "Agent Smith": {
            "id": 3,
            "name": "Hugo Weaving",
            "gender": "MALE",
            "dateOfBirth": "1960-04-04"
        },
        "Morpheus": {
            "id": 2,
            "name": "Laurence Fishburne",
            "gender": "MALE",
            "dateOfBirth": "1961-07-30"
        },
        "Thomas A. Anderson / Neo": {
            "id": 1,
            "name": "Keanu Reeves",
            "gender": "MALE",
            "dateOfBirth": "1964-09-02"
        }
    }
}

1 Ответ

0 голосов
/ 26 апреля 2019

Хороший вопрос: Вы должны сделать немного больше «глубокого погружения», чтобы понять, что происходит. В данном случае это не вопрос дизайна REST. Я не знаю, были ли вы удивлены, увидев актерский состав с информацией о фильме, но вы должны были это сделать.

Ваш код return repository.findById(id) … явно предназначен для извлечения только информации о фильме, но вы заметили, что также получаете актерский состав. Вы печатали операторы sql из системы Spring Data Jpa, чтобы увидеть, работает ли она так, как вы ожидали? Я подозреваю, что нет, потому что если бы вы имели, вы, вероятно, заметили бы, что несколько операторов SQL генерируются. Сначала для фильма, а потом и для актеров.

Как только вы проследите, почему вы получаете несколько SQL-операторов, вы обнаружите, что эти операторы исходят из преобразования Entity-> JSON. Это означает, что когда ваш сервис возвращает сущность Movie, среда Spring должна преобразовать ее в JSON для отправки по проводам, и этот код проходит по графу объектов. Граф объектов - это экземпляры сущностей, которые вы создали в JVM, когда запрашивали базу данных. Поскольку вы сопоставили отношение Movie/Cast, объект фильма включает в себя возможные ссылки на приведение, и когда код преобразования JSON выполняет выборку для свойства преобразования, JPA обнаруживает, что выдает другой запрос, поскольку среда Spring по-прежнему удерживает транзакцию базы данных в области видимости. Если бы транзакция была вне контекста, вы бы получили исключение LazyInitialization. Все это вы должны исследовать немного больше, чтобы вы поняли это.

Итак, как вам сделать лучший дизайн? У вас есть как минимум две возможности, которые приходят на ум. Во-первых, вы можете удалить отображение cast. Как вы думаете, зачем вам нужна коллекция cast в фильме? Если вы хотите получить актерский состав для фильма, вы можете просто позвонить castRepository.findByMovie(movie) и получить список. Во-вторых, вы можете использовать DTO или Data Transfer Object, который представляет собой отдельный POJO, который определяет, что вы хотите, чтобы ваш интерфейс REST действительно возвращал. В этом случае это может быть класс MovieDto, который совпадает с классом сущности Movie, но без свойства cast. Затем вы изменили бы свой movieRepository на метод, определенный как Optional<MovieDto> findById(Long id), и spring-data-jpa будет использовать функцию Проекции для автоматического преобразования вашего объекта Movie в объект MovieDto.

Использование функции Проекции будет моим рекомендуемым подходом. DTO предназначены для «представлений» бизнес-уровня вашего приложения. Разные потребители вашего сервиса могут хотеть разных взглядов на мир кино. Агентам по кастингу может потребоваться список всех фильмов, в которых появился актер, в то время как кинокритики могут захотеть получить список актеров, а любителям кино и кино может просто потребоваться информация о фильме. Все разные DTO или представления для одной и той же базы данных. Я также внимательно рассмотрю вопрос о том, действительно ли вам нужно отображение cast. Я заметил, что у вас есть сегмент кода cast = new HashMap<>();, который создает HashMap для стороны фильма, но вам это не нужно, и в типичном случае чтения он будет выброшен, что приведет к нагрузке на сборщик мусора. Наконец я заметил, что вы определили приведение как карту, но почему вы это сделали? Что происходит в ключе карты, в названии фильма? Это плохой дизайн. Актеры могут появляться в более чем одном фильме, а фильмы могут иметь более одного актера, поэтому у вас должно быть отношение «многие ко многим».

Наконец, вот почему в SO так не одобряются вопросы "это хороший дизайн". Ответы сложны и, как правило, самоуверенны, а не для чего предназначен SO.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...