JPA 级联保存实体中的子实体

1、概览

本文将带你了解 JPA 如何自动保存复杂的实体模型(即由父实体和子实体元素组成的复杂模型)以及常见的问题。

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(级联类型)

使用 @OneToMany 注解创建父实体后,父子关系就可以在持久化上下文中访问了。

3.1、使用 @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、双向关联

给父实体添加双向关联:

@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(级联类型)

知道了问题所在后,就好解决了,只需在单向和双向关系中都使用 CascadeType 即可。

4.1、使用 @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、双向关联

对于双向关系,修改的地方和上节一样。从修改 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 可以看到 BidirectionalChildWithCascadeTypeINSERT 查询将 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、总结

本文介绍了在使用 JPA 保存父实体时,子实体未自动保存的原因,这些原因在单向和双向关系中可能有所不同。

可以使用 CascadeType.PERSIST 来简化这一关联保存的逻辑。如果需要自动更新或删除,还可以考虑其他级联类型。


Ref:https://www.baeldung.com/jpa-save-child-objects-automatically