为什么仅对当前事务之前持久存在的实体自动初始化实体未初始化集合?

时间:2017-10-20 16:27:12

标签: java-8 openjpa jta jboss-arquillian tomee-7

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

我在实体@ManyToOneParent之间进行了非常简单的Child双向映射。

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

EntityManager.find(...)用于以前保留的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;


}

2 个答案:

答案 0 :(得分:1)

如果您创建了Parent,则系统不会初始化该集合,因为您不会这样做。而且,当持久保存父JPA时,将保留该集合。

但是当您使用Hibernate读取Parent时,该集合将包含一个代理,因为许多关系都被提取LAZY,并且此代理用于按需获取子代。

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

答案 1 :(得分:1)

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

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

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

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

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

最终事务提交并且取决于您的提供者,实体可以缓存在二级缓存中。我假设你不是为了简洁而使用它;除非你强制EM刷新这个实例(如果它是一个新的!先刷新!)或者在一个单独的EntityManager中读取它,你将总是使用任何空引用获得相同的实例。

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