0

(请在阅读此问题后随时编辑标题)

@ManyToOne在实体ParentChild.

Collection<Child> children中的子项列表Parent从未初始化,因此应该是null.

当使用EntityManager.find(...)for 之前持久化Parent然后从中获取列表时,Parent即使没有孩子,它也会给出 ArrayList,这Parent很好。

但是,如果Parent在同一个事务集合中持久化或合并新的子元素,null即使持久化/合并Parent再次使用EntityManager.find(...).

所以我想知道这种不同的行为,以及它是否只发生在我的环境中。

我认为它与实体的缓存有关:从缓存中找到实体并返回它而不是从 db 中获取它,并且空集合的初始化只会在从 db 中获取时发生,可能取决于 JPA 实现。

我的假设是否接近事实?如果不是,原因是什么?

下面的实体和测试用例。我的测试环境在标签中列出。

// using lombok
@Slf4j
@RunWith(Arquillian.class)
public class NoPersistTest {

    @PersistenceContext
    private EntityManager em;

    @Deployment
    public static final WebArchive deploy() {
        WebArchive wa = ShrinkWrap.create(WebArchive.class, "test.war")
                .addAsWebInfResource("test-persistence.xml", "persistence.xml").addClasses(Parent.class, Child.class);
        return wa;
    }

    @Test
    @Transactional
    public void testWithPreviouslyPersistedParent() {
        Parent parent = em.find(Parent.class, 1); // has no children in db
                                                    // before
        Child child = new Child();
        child.setParent(parent);
        parent.getChildren().add(child);
        log.info("type of Collection<Child> is {}", parent.getChildren().getClass().getName());
        // above logs "type of Collection<Child> is
        // org.apache.openjpa.util.java$util$ArrayList$proxy"
    }

    @Test(expected = NullPointerException.class)
    @Transactional
    public void testPersistingParentInSameTransaction() {
        Parent parent = new Parent();
        em.persist(parent);
        Parent parent2 = em.find(Parent.class, parent.getId());
        Child child = new Child();
        child.setParent(parent2);
        log.info("Collection<Child> is {}", parent2.getChildren());
        // above logs Collection<Child> is null
        parent2.getChildren().add(child);
    }

    @Test(expected = NullPointerException.class)
    @Transactional
    public void testMergingParentInSameTransaction() {
        Parent parent = new Parent();
        parent = em.merge(parent);
        Parent parent2 = em.find(Parent.class, parent.getId());
        Child child = new Child();
        child.setParent(parent2);
        log.info("Collection<Child> is {}", parent2.getChildren());
        // logs Collection<Child> is null
        parent2.getChildren().add(child);
    }

}

@Entity @Getter @Setter
public class Parent {

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

    @OneToMany(mappedBy="parent", cascade=CascadeType.ALL, orphanRemoval=true)
    private Collection<Child> children;

    private Date created = new Date(); // just to have something to persist

}

@Entity @Getter @Setter
public class Child {

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

    private Date created = new Date(); // just to have something to persist

    @ManyToOne(optional=false)
    private Parent parent;


}
4

2 回答 2

1

如果您创建父集合,则不会初始化集合,因为您不这样做。并且在持久化父 JPA 时也会将集合保持原样。

但是,当您使用 Hibernate 读取 Parent 时,该集合将包含一个代理,因为 toMany 关系是 LAZY 获取的,并且该代理用于按需获取子级。

我的建议是始终初始化集合以避免 NullPointerExceptions。这是很好的编程风格。

于 2017-10-20T16:33:08.453 回答
1

下面的答案是正确的,我只想在其他地方的评论中添加一些更多信息。

JPA 在可能的情况下使用缓存来避免数据库命中,并且在仍然需要数据库命中的情况下,缓存避免了重建对象的成本并允许维护身份 - 确保在遍历 A->B->A 循环引用时返回相同的 A 实例.

当您持久化一个实体时,您将它作为托管实体放置在 EntityManager 缓存中 - 在该 EntityManager 上调用 find 将返回您刚刚传入的完全相同的实例。

  A initialA = new A();
  A managedA = em.persist(initialA);
  managedA==initialA

持久调用本身不会更改实体中的任何内容(如果允许使用预分配的序列,则可能是 ID 除外),因此任何空引用仍将为空。

最终事务提交并且根据您的提供者,实体可以缓存在二级缓存中。为了简洁起见,我假设您没有使用它;除非您强制 EM 刷新此实例(如果它是新实例,请先刷新!)或在单独的 EntityManager 中读取它,否则您将始终使用任何空引用返回同一个实例。

如果您刷新它或以其他方式导致它重新加载,则您的 JPA 提供程序需要根据您的映射将对象中的所有内容设置为数据库中的所有内容。由于 null 不是集合映射的持久状态,这意味着它要么急切地获取您的引用,要么在其中放置代理以实现惰性关系,从而导致您找到一个空集合。

于 2017-11-10T17:13:56.907 回答