(请在阅读此问题后随时编辑标题)
我在实体@ManyToOne
和Parent
之间进行了非常简单的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;
}
答案 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不是集合映射的可持久状态,这意味着它将急切地获取您的引用,或者将代理放在那里以实现惰性关系,从而导致您找到一个空集合。