【问题标题】:How to properly design RestController for spring REST API with JPA?如何使用 JPA 为 Spring REST API 正确设计 RestController?
【发布时间】:2019-04-26 13:13:00
【问题描述】:

我不知道如何正确设计电影数据库 Web 应用后端的架构。

我有一个电影实体,它有一个流派实体列表和一个演员实体地图以及他们在电影中的角色:

// ...

@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"
        }
    }
}

【问题讨论】:

    标签: spring rest jpa


    【解决方案1】:

    好问题:您应该对此做更多的“深入研究”以了解发生了什么。在这种情况下,这不是 REST 设计的问题。我不知道你是否对电影信息中包含的演员感到惊讶,但你应该感到惊讶。

    您的代码 return repository.findById(id) … 似乎显然只用于检索电影信息,但您注意到您还获得了演员表。您是否打印了来自Spring Data Jpa 系统的 sql 语句以查看它是否按照您预期的方式运行?我怀疑不是,因为如果你有你可能会注意到正在生成几个 sql 语句。首先是电影,然后是演员的后续声明。

    一旦你追查到为什么你会得到多个 sql 语句,你会发现这些语句来自 Entity->JSON 转换。这意味着当您的服务返回 Movie 实体时,spring 框架需要将其转换为 JSON 以通过网络发送它,并且该代码正在遍历对象图。对象图是您查询数据库时在 JVM 中创建的实体的实例。由于您已经映射了Movie/Cast 关系,因此电影对象包括对演员表的可能引用,并且当 JSON 转换代码对演员表属性进行获取时,JPA 检测到发出另一个请求,因为 Spring 框架仍在范围内持有数据库事务。如果事务超出范围,您将获得LazyInitialization 异常。所有这些你都应该多研究一下,这样你就明白了。

    那么,您如何进行更好的设计呢?您至少想到了两种可能性。首先,您可以删除 cast 映射。为什么你认为你需要将cast 作为电影中的一个集合?如果您想获得一部电影的演员表,您只需致电castRepository.findByMovie(movie) 并获取名单。其次,您可以使用 DTO 或数据传输对象,它是一个单独的 POJO,它定义了您希望 REST 接口实际返回的内容。在这种情况下,它可能是一个 MovieDto 类,它与 Movie 实体类相同,但没有 cast 属性。然后您将更改您的movieRepository 以将方法定义为Optional&lt;MovieDto&gt; findById(Long id) 和spring-data-jpa 将使用Projections 功能将您的Movie 实体自动转换为MovieDto 对象。

    使用Projections 功能将是我推荐的方法。 DTO 用于应用程序业务层的“视图”。您服务的不同消费者可能希望对电影世界有不同的看法。选角代理可能想要一个演员出演的所有电影的列表,而影评人可能想要一个演员列表以及电影和电影爱好者可能只想要电影的细节。同一数据库的所有不同 DTO 或视图。我还会仔细考虑您是否真的需要 cast 映射。我注意到你有一个cast = new HashMap&lt;&gt;(); 代码段,它为关系的电影方面创建了一个HashMap,但你不应该需要它,并且在典型的读取用例中,它会被丢弃,给垃圾收集器带来压力。最后我注意到您将 cast 定义为 Map 但您为什么要这样做?地图的键是什么,电影名称?那是糟糕的设计。演员可以出现在不止一部电影中,电影也可以有不止一个演员,所以你应该有一个多对多的关系。

    最后,这就是为什么在 SO 中不赞成“这是一个好的设计”问题的原因。答案很复杂,而且通常是固执己见,而不是 SO 的目的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-28
      • 1970-01-01
      • 2019-10-22
      相关资源
      最近更新 更多