Spring Boot Rest/JPA:仅创建一个父实体,在将子实体的创建级联到父实体的创建时
我需要用Spring Boot 3.x与 Spring Data JPA(Hibernate)来创建一个RESTful API。
我有如下场景。
我需要创建子实体,并将持久化/合并操作级联到父实体。
这意味着:
- 创建 "Child_1" 属于 "Parent_1",并级联创建其父实体 "Parent_1"
- 创建 "Child_2" 属于 "Parent_1",并级联创建其父实体 "Parent_1"
请注意,"Child_1"、"Child_2" 是 "Parent_1" 的子对象。
因此,我想要如下所示的场景:
[所需场景]
Child:
id name parent_id
1 child_1 1
2 child_2 1
Parent:
id name
1 parent_1
不幸的是,结果如下:
[结果场景]
Child:
id name parent_id
1 child_1 1
2 child_2 2
Parent:
id name
1 parent_1
2 parent_1
在每次创建子对象时,都会级联创建一个新的父对象。
下面是基础代码。若你需要一个用例,请把Parent/Post、Child/Comment替换为在JPA/Hibernate专家Vlad Mihalcea的示例。
有懂行的人能给我一个解决方案吗?
如何通过从子对象到父对象的级联来实现 [所需场景]?
先行致谢
@RestController
public class ChildController {
private final ChildService childService; //Autowired
@PostMapping
public ChildResponseDto saveChild(@RequestBody ChildRequestDto childRequestDto) {
ChildResponseDto childResponseDto = childService.saveChild(childRequestDto);
return childResponseDto;
}
}
@Service
public class ChildService {
private final ChildRepository childRepository; // Autowired
private final ChildMapper childMapper; // Autowired
public ChildResponseDto saveChild(ChildRequestDto childRequestDto) {
Child child = childMapper.toChild(childRequestDto);
Child savedChild = childRepository.save(child);
return childMapper.fromChild(savedChild);
}
}
@Service
public class ChildMapper {
private final ParentMapper parentMapper; // Autowired
public ChildResponseDto fromChild(Child child) {
return ChildResponseDto.builder()
.id(child.getId())
.name(child.getName())
.parentResponseDto(parentMapper.fromParent(child.getParent()))
.build();
}
public Child toChild(ChildRequestDto childRequestDto) {
Parent parent = parentMapper.toParent(childRequestDto.parentRequestDto());
return Child.builder()
.name(childRequestDto.name())
.parent(parent)
.build();
}
}
@Entity
public class Parent {
@Id
private Integer id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id
private Long id;
private String name;
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinColumn(name = "parent_id")
private Parent parent;
}
解决方案
为什么会这样?
在 toChild 方法中,你将请求的DTO映射为Parent和 Child对象,然后通过调用 save 方法来持久化Child对象。
Hibernate对实体有生命周期,实体有4 种状态(更多细节请看这里)。 在这个生命周期中,实体会处于不同的状态,Hibernate的实体状态共有4 种:
- 临时态
- 持久态
- 分离态
- 移除态
在你的代码中,Parent和 Child实体都处于 Transient 状态,因为它们是新的对象,Hibernate不知道已有的Parent实体,因此当Hibernate尝试持久化这些对象时,会把它们都视为新的对象,然后将它们作为新的记录写入数据库。
解决方案
要解决这个问题,你需要让Hibernate知道数据库中已经存在一个父实体实例,这意味着在将Child实例持久化到数据库之前,应该从数据库中加载Parent实体,而不是新建一个对象。
当你从数据库加载一个Parent实体时,这个Parent实体的状态是 Persistent state 而不是 Transient State,这意味着Hibernate会跟踪对象的状态,也就是说Hibernate知道该实体存在于数据库中,不会为其创建新的实例。
伪代码将大致如下:
Parent parent = parentRepository.load(parentId);
Child child = childMapper.toChild(childRequestDto);
child.setParent(parent);
childRepository.save(child);