【问题标题】:Why Entitys uninitialized collection is initialized automatically only for Entities persisted before current transaction?为什么只有在当前事务之前持久化的实体才会自动初始化实体未初始化集合?
【发布时间】:2017-10-20 16:27:11
【问题描述】:

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

我在实体ParentChild 之间有相当简单的@ManyToOne 双向映射。

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

当将EntityManager.find(...) 用于先前持久化的Parent,然后从该Parent 获取列表时,即使该Parent 还没有子级,也可以得到ArrayList,这很好。

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

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

我认为它与实体的缓存有关:从缓存中找到实体并返回它而不是从 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;


}

【问题讨论】:

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


    【解决方案1】:

    如果您创建了 Parent,则集合不会被初始化,因为您没有这样做。并且在持久化 Parent JPA 时也会保持集合不变。

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

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

    【讨论】:

    • 你的建议是我一直做的,因为其他地方会有很多 NPE :) 但是如果我们假设我的 openjpa 和 hibernate 一样,我想知道为什么它不能在同一个中初始化空集合在我的两个 NPE 测试用例中和工作用例中的方式一样吗?
    • 因为这两个测试用例中没有理由改变集合的内容,因为不需要懒惰的获取。
    • 并且没有理由延迟加载,因为 JPA 实际上返回相同的代理 parent/parent2,尽管我非常努力地尝试从 db 中获取它或者为什么(对不起,如果很慢......)?用这个“没有理由”的更多细节更新答案,我会接受的。
    • 您的答案还可以,但也许从我想要的不同的角度来看:我在这里的原因很可能是:ReloadableEntityManagerFactory我的 openjpa 使用缓存每个事务的实体,所以当我尝试调用 em.find()(即使使用另一个 em2)EMF 返回相同的代理,并且没有必要初始化集合,所以它没有完成(即使它会很好)。 LAZY fetch 需要初始化,只有在从数据库中查询时才会完成。
    • 如果你想重新加载你的实体调用 EntityManager.refresh() 并且你的集合将被初始化。
    【解决方案2】:

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

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

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

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

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-12-26
      • 2021-12-18
      • 1970-01-01
      • 2014-01-29
      • 1970-01-01
      • 2014-07-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多