1. 概述

有时我们需要处理由父元素和子元素组成的复杂实体模型。在这种情况下,自动保存父实体时,可能会顺带保存所有子实体是有益的。本教程将深入探讨如何实现这一目标,我们将讨论单向和双向关联

2. 忽略的关系注解

首先可能被忽略的是添加关系注解。 我们创建一个子实体:

@Entity
public class BidirectionalChild {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    //getters and setters
}

现在,我们创建一个包含BidirectionalChild实体的父实体:

@Entity
public class ParentWithoutSpecifiedRelationship {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private List<BidirectionalChild> bidirectionalChildren;
    //getters and setters
}

如图所示,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注解。 让我们创建Parent实体:

@Entity
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @OneToMany
    @JoinColumn(name = "parent_id")
    private List<UnidirectionalChild> joinColumnUnidirectionalChildren;
    //getters and setters
}

现在,创建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实体,将其存储到数据库中,并清空了持久化上下文。但当我们尝试检查从数据库获取的父实体是否包含预期的所有子实体时,可以看到子实体列表为空。

让我们查看JPA生成的SQL查询:

Hibernate: 
    insert 
    into
        Parent
        (id) 
    values
        (?)
Hibernate: 
    update
        UnidirectionalChild 
    set
        parent_id=? 
    where
        id=?

我们可以看到对两个实体的修改查询,但UnidirectionalChild实体的INSERT查询缺失。

3.2. 双向关系

现在,让我们在Parent实体中添加双向关系:

@Entity
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @OneToMany(mappedBy = "parent")
    private List<BidirectionalChild> bidirectionalChildren;
    //getters and setters
}

这是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;
    //getters and setters
}

UnidirectionalChild保持不变。现在,尝试保存带有相关UnidirectionalChild实体的ParentWithCascadeType

@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=?

我们可以看到UnidirectionalChildINSERT查询存在。

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;
}

现在,尝试保存带有相关BidirectionalChildWithCascadeTypeParentWithCascadeType

@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();
}

我们对UnidirectionalChild做了同样的改动,并期待类似的行为。但不知何故,我们遇到了一个子实体列表为空的问题。首先,让我们查看SQL查询日志:

Hibernate: 
    insert 
    into
        ParentWithCascadeType
        (id) 
    values
        (?)
Hibernate: 
    insert 
    into
        BidirectionalChildWithCascadeType
        (parent_id, id) 
    values
        (?, ?)

日志中显示了所有预期的查询。调试问题后,我们注意到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来实现这种逻辑。如果需要自动更新或删除,我们还可以考虑其他级联类型。

如往常一样,完整的源代码可以在GitHub上找到。