Spring Data Jpa OneToMany如何同时保存父子实体?

16

这是我的父实体。 注意:为了简洁起见,移除了getter、setter和lombok注释。

@Entity
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @OneToMany(mappedBy = "board")
    private Set<Story> stories = new HashSet<>();
}

以下是我的子实体

@Entity
public class Story {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "board_id")
    @JsonIgnore
    private Board board;
}
每个Board可以有多个Story,但每个Story只属于一个Board
现在,在我的服务中的某个地方,我正在执行以下操作:
public void addBoard(BoardDTO boardDto){
    // create a new board object which is just a pojo
    // by copying properties from boardDto
    Board board = ...;

    // create set of stories
    List<String> defaultTitles = Arrays.asList("Todo", "In-Progress", "Testing", "Done");
    Set<Story> stories = defaultTitles.stream().map(title -> Story.builder()
            .title(title)
            // assign a reference, I know this is wrong since board here is not
            // saved yet or fetched from db, hence the question
            .board(board) 
            .build())
            .collect(Collectors.toSet());

    // This saves board perfectly, but in Story db, the foreign key column
    // board_id is null, rightfully so since call to story table was not yet done.
    Board save = boardRepository.save(Board.builder()
            .title(board.getTitle())
            .stories(stories)
            .build());
}

我可以采取的一种方法是首先保存没有 Set<Story> 的板子,然后使用此保存的板子将故事进行保存。但这将需要两个存储库调用,并且在代码方面看起来不好。

另外,我遇到麻烦的原因是在运行此代码之前,我的数据库是空的。也就是说,这是我们第一次输入的新记录。所以 Board table 还没有行。

那么有没有办法一次完成这个操作呢?对于stackoverflow上的大多数其他问题,板实体已经从数据库中获取,然后他们向其添加子实体并将其保存到数据库中。但对于我来说,数据库完全是新的,我想一次添加一个新的父实体及其相应的子实体,至少在代码方面,即使Hibernate进行多个数据库调用。

1个回答

7
是的,您只需将更改从父级级联到子级即可:
@Entity
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @OneToMany(mappedBy = "board", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private Set<Story> stories = new HashSet<>();
}

现在,每次保存父项(Board)时,更改将级联到子表。您还可以使用CascadeType.ALL代替{CascadeType.PERSIST, CascadeType.MERGE}来级联任何更改,例如删除(当您从父实体的集合中删除子项时,子表中的连接ID也将被删除)。

非常感谢。我只有一个小问题。正如您在我的问题中所看到的,我在子实体上有一个级联。因此,在我的父表中,正在添加两行。当然,我可以删除子实体上的级联。但是如果我想要那个怎么办?我如何避免将两行持久化到我的父表中的这个问题? - theprogrammer
如果我在子实体上删除级联,就会出现错误org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing。这可能是因为Story只是一个普通的POJO,并没有被保存。另一方面,如果我保留子实体上的级联(以及您为父实体提供的级联),则会在父实体中看到两行记录。而你所说的更新是什么意思?你是指jpa.show-sql: true的更新吗? - theprogrammer
1
所以我刚刚检查了日志。有两个插入发送到Board(父级),并且有4个插入到Story(子级)。子实体中的4个插入是有道理的,因为我的defaultTitles列表中有4个元素。但是父级的第二个插入很奇怪。这也搞乱了子表中的board_id外键列,而不是1,现在是2(因为被推入父表的第二行)。 - theprogrammer
由于讨论超出了答案的范围,我们能否在 https://chat.stackoverflow.com/rooms/207050/spring-data-jpa-onetomany-save-child-and-parent-entities-at-the-same-time 继续讨论? - Andronicus
@theprogrammer 遇到了类似的问题。你能解决这个问题吗? 聊天室链接似乎无法使用。 - ragav ramachandran
3
@ragavramachandran,原帖使用此处描述的解决方案,并且成功了:https://vladmihalcea.com/the-best-way-to-map-a-onetomany-association-with-jpa-and-hibernate/ - Andronicus

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