1、概览 {#1概览}
本文将带你了解 JPA 如何自动保存复杂的实体模型(即由父实体和子实体元素组成的复杂模型)以及常见的问题。
2、缺失关系注解 {#2缺失关系注解}
我们可能会忽略的第一件事就是添加关系注解。
创建一个子实体:
@Entity
public class BidirectionalChild {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
//Get/Set 方法省略
}
创建一个包含 List<BidirectionalChild>
的父实体:
@Entity
public class ParentWithoutSpecifiedRelationship {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private List<BidirectionalChild> bidirectionalChildren;
//Get/Set 方法省略
}
如上,bidirectionalChildren
字段上没有注解。尝试用这些实体创建一个 EntityManagerFactory
:
@Test
void givenParentWithMissedAnnotation_whenCreateEntityManagerFactory_thenPersistenceExceptionExceptionThrown() {
PersistenceException exception = assertThrows(PersistenceException.class,
() -> createEntityManagerFactory("jpa-savechildobjects-parent-without-relationship"));
assertThat(exception)
.hasMessage("Could not determine recommended JdbcType for Java type 'com.baeldung.BidirectionalChild'");
}
运行测试,出现了异常,无法确定子实体的 JdbcType
。单向和双向关系都会出现类似的异常,其根本原因是父实体中缺失 @OneToMany
注解。
3、未指定 CascadeType(级联类型) {#3未指定-cascadetype级联类型}
使用 @OneToMany
注解创建父实体后,父子关系就可以在持久化上下文中访问了。
3.1、使用 @JoinColumn 建立单向关联 {#31使用-joincolumn-建立单向关联}
使用 @JoinColumn
注解建立单向关系,创建父实体:
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToMany
@JoinColumn(name = "parent_id")
private List<UnidirectionalChild> joinColumnUnidirectionalChildren;
// Get /Set 方法
}
然后,创建 UnidirectionalChild
实体:
@Entity
public class UnidirectionalChild {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
}
最后,尝试保存包含几个子实体的 Parent
实体:
@Test
void givenParentWithUnidirectionalRelationship_whenSaveParentWithChildren_thenNoChildrenPresentInDB() {
Parent parent = new Parent();
List<UnidirectionalChild> joinColumnUnidirectionalChildren = new ArrayList<>();
joinColumnUnidirectionalChildren.add(new UnidirectionalChild());
joinColumnUnidirectionalChildren.add(new UnidirectionalChild());
joinColumnUnidirectionalChildren.add(new UnidirectionalChild());
parent.setJoinColumnUnidirectionalChildren(joinColumnUnidirectionalChildren);
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
entityManager.persist(parent);
entityManager.flush();
transaction.commit();
entityManager.clear();
Parent foundParent = entityManager.find(Parent.class, parent.getId());
assertThat(foundParent.getChildren()).isEmpty();
}
如上,创建了一个有三个子实体的 Parent
实体,将其存储在数据库中,并清除了持久化上下文。但是,当我们尝试验证从数据库中获取的 Parent
实体是否包含所有预期的子实体时,我们发现子实体列表是空的。
JPA 生成的 SQL 查询如下:
Hibernate:
insert
into
Parent
(id)
values
(?)
Hibernate:
update
UnidirectionalChild
set
parent_id=?
where
id=?
我们可以看到两个实体都有修改查询,但 UnidirectionalChild
实体没有 INSERT
查询。
3.2、双向关联 {#32双向关联}
给父实体添加双向关联:
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToMany(mappedBy = "parent")
private List<BidirectionalChild> bidirectionalChildren;
// Getter / Setter
}
BidirectionalChild
实体如下:
@Entity
public class BidirectionalChild {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne
private Parent parent;
}
BidirectionalChild
实体包含对 Parent
实体的引用。
尝试保存具有双向关系的复杂对象:
@Test
void givenParentWithBidirectionalRelationship_whenSaveParentWithChildren_thenNoChildrenPresentInDB() {
Parent parent = new Parent();
List<BidirectionalChild> bidirectionalChildren = new ArrayList<>();
bidirectionalChildren.add(new BidirectionalChild());
bidirectionalChildren.add(new BidirectionalChild());
bidirectionalChildren.add(new BidirectionalChild());
parent.setChildren(bidirectionalChildren);
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
entityManager.persist(parent);
entityManager.flush();
transaction.commit();
entityManager.clear();
Parent foundParent = entityManager.find(Parent.class, parent.getId());
assertThat(foundParent.getChildren()).isEmpty();
}
和上一节一样,这里也没有保存子项目。可以在日志中到如下查询:
Hibernate:
insert
into
Parent
(id)
values
(?)
原因是没有为关系指定 CascadeType (级联类型)。如果希望自动保存父实体和子实体,就必须指定级联类型。
4、设置 CascadeType(级联类型) {#4设置-cascadetype级联类型}
知道了问题所在后,就好解决了,只需在单向和双向关系中都使用 CascadeType 即可。
4.1、使用 @JoinColumn 建立单向关联 {#41使用-joincolumn-建立单向关联}
在 ParentWithCascadeType
实体中的单向关系中添加 CascadeType.PERSIST
:
@Entity
public class ParentWithCascadeType {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToMany(cascade = CascadeType.PERSIST)
@JoinColumn(name = "parent_id")
private List<UnidirectionalChild> joinColumnUnidirectionalChildren;
// Getter/Setter 省略
}
UnidirectionalChild
保持不变。现在,尝试保存 ParentWithCascadeType
实体以及与其相关的几个 UnidirectionalChild
实体:
@Test
void givenParentWithCascadeTypeAndUnidirectionalRelationship_whenSaveParentWithChildren_thenAllChildrenPresentInDB() {
ParentWithCascadeType parent = new ParentWithCascadeType();
List<UnidirectionalChild> joinColumnUnidirectionalChildren = new ArrayList<>();
joinColumnUnidirectionalChildren.add(new UnidirectionalChild());
joinColumnUnidirectionalChildren.add(new UnidirectionalChild());
joinColumnUnidirectionalChildren.add(new UnidirectionalChild());
parent.setJoinColumnUnidirectionalChildren(joinColumnUnidirectionalChildren);
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
entityManager.persist(parent);
entityManager.flush();
transaction.commit();
entityManager.clear();
ParentWithCascadeType foundParent = entityManager
.find(ParentWithCascadeType.class, parent.getId());
assertThat(foundParent.getJoinColumnUnidirectionalChildren())
.hasSize(3);
}
与前几节一样,创建了父实体,添加了几个子实体,并将其在事务中保存。
SQL 日志如下:
Hibernate:
insert
into
ParentWithCascadeType
(id)
values
(?)
Hibernate:
insert
into
UnidirectionalChild
(id)
values
(?)
Hibernate:
update
UnidirectionalChild
set
parent_id=?
where
id=?
如上,UnidirectionalChild
实体也执行了 INSERT
查询。
4.2、双向关联 {#42双向关联}
对于双向关系,修改的地方和上节一样。从修改 ParentWithCascadeType
实体开始:
@Entity
public class ParentWithCascadeType {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<BidirectionalChildWithCascadeType> bidirectionalChildren;
}
现在,尝试保存 ParentWithCascadeType
实体以及与之相关的几个 BidirectionalChildWithCascadeType
实体:
@Test
void givenParentWithCascadeTypeAndBidirectionalRelationship_whenParentWithChildren_thenNoChildrenPresentInDB() {
ParentWithCascadeType parent = new ParentWithCascadeType();
List<BidirectionalChildWithCascadeType> bidirectionalChildren = new ArrayList<>();
bidirectionalChildren.add(new BidirectionalChildWithCascadeType());
bidirectionalChildren.add(new BidirectionalChildWithCascadeType());
bidirectionalChildren.add(new BidirectionalChildWithCascadeType());
parent.setChildren(bidirectionalChildren);
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
entityManager.persist(parent);
entityManager.flush();
transaction.commit();
entityManager.clear();
ParentWithCascadeType foundParent = entityManager
.find(ParentWithCascadeType.class, parent.getId());
assertThat(foundParent.getChildren()).isEmpty();
}
运行测试,先保存实体,然后再检索实体。看似一切正常,但是最后检索到的子列表是空的?SQL 查询日志如下:
Hibernate:
insert
into
ParentWithCascadeType
(id)
values
(?)
Hibernate:
insert
into
BidirectionalChildWithCascadeType
(parent_id, id)
values
(?, ?)
在日志中,可以看到所有预期的查询都存在。通过 DEBUG 可以看到 BidirectionalChildWithCascadeType
的 INSERT
查询将 parent_id
设置为 null
。出现这个问题的原因是,对于双向关联,需要明确指定父实体的引用。通常的做法是在父实体中指定:
@Entity
public class ParentWithCascadeType {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<BidirectionalChildWithCascadeType> bidirectionalChildren;
public void addChildren(List<BidirectionalChildWithCascadeType> bidirectionalChildren) {
this.bidirectionalChildren = bidirectionalChildren;
this.bidirectionalChildren.forEach(c -> c.setParent(this));
}
}
在这个方法中,将子实体列表的引用设置为父实体,并为每个子实体设置父实体的引用。
现在,尝试使用新方法来保存这个父实体并设置其子实体:
@Test
void givenParentWithCascadeType_whenSaveParentWithChildrenWithReferenceToParent_thenAllChildrenPresentInDB() {
ParentWithCascadeType parent = new ParentWithCascadeType();
List<BidirectionalChildWithCascadeType> bidirectionalChildren = new ArrayList<>();
bidirectionalChildren.add(new BidirectionalChildWithCascadeType());
bidirectionalChildren.add(new BidirectionalChildWithCascadeType());
bidirectionalChildren.add(new BidirectionalChildWithCascadeType());
parent.addChildren(bidirectionalChildren);
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
entityManager.persist(parent);
entityManager.flush();
transaction.commit();
entityManager.clear();
ParentWithCascadeType foundParent = entityManager
.find(ParentWithCascadeType.class, parent.getId());
assertThat(foundParent.getChildren()).hasSize(3);
}
测试通过,父实体成功保存了所有子实体,并且成功地从数据库中关联检索到了保存的子实体。
5、总结 {#5总结}
本文介绍了在使用 JPA 保存父实体时,子实体未自动保存的原因,这些原因在单向和双向关系中可能有所不同。
可以使用 CascadeType.PERSIST
来简化这一关联保存的逻辑。如果需要自动更新或删除,还可以考虑其他级联类型。
Ref:https://www.baeldung.com/jpa-save-child-objects-automatically