使用Spring Data JPA处理POST请求中的关系

3
我有以下实体,并且每个实体都有一个对应的CrudRepository

@Entity
class Movie {
    @Id Long id;
    @Column String name;
    @ManyToOne Person director;
}

@Entity
class Person {
    @Id Long id;
    @Column String name;
}

我的控制器看起来像这样:

@RestController
@RequestMapping("/movies")
class MovieController {

    private MovieRepository movies = ...;
    private PersonRepository people = ...;

    @PostMapping
    public Movie create(@RequestBody MovieRequest request) {
        // Get the director
        Person director = people.findById(request.directorId);

        // Create the new movie
        Movie movie = new Movie();
        movie.name = request.name;
        movie.director = director;

       // Save the new movie
       return movies.save(movie);
    }
}

class MovieRequest {
    String name;
    Long directorId
}

如您所见,create方法首先通过其id加载导演,然后创建新电影,最后保存它。这将导致两次数据库查询:第一次检索导演,第二次保存电影。

在这种情况下,这不是一个大问题,但可能存在具有许多关联的实体,这意味着为了实现单个插入可能需要执行许多查询。

问题:我想在单个数据库操作中保存新电影。有没有办法避免最初的人员查询?有没有更好的处理这种情况的方法?


1
你是否熟悉findById和getOne之间的区别?由于你只需要导演的引用而不是整个对象,请查看此问题https://dev59.com/IW035IYBdhLWcg3wVuh1 - C. Weber
谢谢@C.Weber,这几乎是我想要的。正如你所提到的,getOne方法比在这种情况下使用findById更好,因为它获取引用而不是整个实体,但它仍然会去数据库检查ID是否存在,并在不存在时抛出EntityNotFoundException。我真的希望能够将其作为单个数据库操作完成,这似乎可能吗? - ESala
你正在使用Hibernate作为JPA提供程序吗?据我所知,只要您不访问标识符以外的任何属性,Hibernate就不会初始化代理。可以通过以下设置hibernate.jpa.compliance.proxy(默认应为false)来更改此行为。如果设置为true,则还将在访问标识符时初始化代理。有关更多信息,请参见Hib用户指南。我猜测您正在访问代理,标志已设置为true或我的信息已过时 :) - C. Weber
@ESala你的MOVIE表中有哪些列? - Supun Wijerathne
@C.Weber 是的,我正在使用Hibernate,但不是直接使用,我在其上使用spring-data-jpa。我刚刚使用PersonRepository扩展JpaRepository并调用getOne(id)进行了测试,但它仍然获取实体。这是记录的查询:select person0_.id as id1_1_0_, person0_.name as name2_1_0_ from person person0_ where person0_.id=? - ESala
3个回答

1
无法告诉代码需要将新的电影关联到哪个“人物”。因此,您需要执行查询并手动进行关联。
如果您的端点同时创建“人物”和“电影”,则有一种可行的替代方法。然后,您可以执行2个保存操作或使用“CascadeType = ALL”进行单个保存操作。
如果您能更改请求参数,接收完整的Person对象而不是接受directorId可能是一个不错的选择。这样,您只需进行关联movie.director = director;即可。但是请注意此种方法:如果接收到的Person对象未存储在数据库中,则会引发异常。
也许你可以为你的 Directors 创建一个缓存。例如,如果你把所有的导演保存在 Redis 中,那么你可以搜索与接收到的 directorId 相应的 Director,然后执行关联操作。
当然,你仍然需要进行第二个操作,但这可能比查询数据库要便宜得多。

1
谢谢你的回答。假设相关实体已经存在。如果我有一个具有10个关系的实体,会发生什么?我需要进行10次查询和保存吗? - ESala
遗憾的是:是的。这主要是因为与您的电影相关的“人物”取决于用户请求参数。我更新了我的答案,并提供了一种在您特定情况下提高性能的替代方案。 - andreybleme
感谢建议使用缓存,但是我仍在等待HTTP请求,即使这应该是一个简单的插入。在我的情况下,在请求中放置完整的“Person”也是不可接受的,因为相关实体可能很大并且还有关系。我真正寻找的是一种在“spring-data-jpa”中处理这个问题的方法。 - ESala
好的 @ESala,在回答中,我尽力包含了我脑海中的所有想法。很抱歉我无法提供帮助。 - andreybleme
谢谢@andreybleme,我非常感激!我现在会把它保持开放状态,看看是否有其他人提出另一个解决方案。 - ESala

0

这可能看起来不太美观,但是由于您的请求中有personId,因此您可以将您的电影与您的长personId进行映射。

class Movie {
    @Id Long id;
    @Column String name;
    @ManyToOne Person director;

    @Column(name="PERSON_ID")
    long personId;
}

在你的控制器中

movie.setPersonId(request.directorId);

你能详细说明一下吗?你所说的“将电影与personId进行映射”是什么意思? - ESala
谢谢您的回答,但这样一来导演字段就不会被更新了。此外,当查询电影详情时,每个关系都需要进行额外的查询。 - ESala
由于导演字段将与您的表中的personId相同,因此它也将被更新。有关详细信息,您将使用@ManyToOne Person director。 - Gnk

0

我认为你的MOVIE表中不包含'DIRECTOR_NAME'列(假设你遵循第二范式规则)。它应该仅仅DIRECTOR_ID

因此,在您的场景中,您完全可以跳过加载导演名称(前提是直接将ID与请求参数一起发送)。

由于您在Movie.DIRECTOR_IDDIRECTOR.ID之间有(应该有)一个外键约束,如果您尝试插入任何不存在的DIRECTOR_ID,它将查找任何约束违规。所以你不需要担心。


是的,这就是我所拥有的,导演被引用为外键director_id。我想要的是如何解决我问题描述末尾的那个问题。 - ESala
@ESala,你可以直接跳过那个findById查询。保存电影到数据库不需要它吧? - Supun Wijerathne
当保存新电影时,您还可以如何设置导演字段? - ESala
@ESala请告诉我你为什么需要那个?你的MOVIE表没有DIRECTOR_NAME列,对吧?因此,将名称设置到模型中是不必要的。 - Supun Wijerathne

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接