Why Entitys uninitialized collection is initialized automatically only for Entities persisted before current transaction?

醉酒当歌 提交于 2019-12-25 08:53:50

问题


(Please feel free to edit the title after reading this question)

I have quite simple @ManyToOne bidirectional mapping between entities Parent and Child.

The list of children Collection<Child> children in Parent is never initialized so it should be null.

When using EntityManager.find(...) for previously persisted Parent and then getting the list from that Parent gives ArrayList even there are no children yet with this Parent and it is fine.

However if persisting or merging a new Parent in the same transaction collection of children will be null even if the persisted/merged Parent is fetched again with EntityManager.find(...).

So i wonder this different behavior and if it is happening only in my environment.

I assume it has something to do with the caching of entities: entity is found from cache and it is returned instead of fetching it from db AND the initialization of empty collections will happen only when fetched from db, maybe depending on the JPA implementation.

Is my assumption even near the truth and if not what is the reason ?

Entities and test cases below. My test environment listed in tags.

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


}

回答1:


If you create the Parent the collection is not initialized because you don't do it. And also when persisting the Parent JPA will leave the collection as it is.

But when you read the Parent with Hibernate the collection will contain a proxy because toMany relationships are fetched LAZY and this proxy is used to fetch the children on demand.

My recommendation is to always initialize collection to avoid NullPointerExceptions. That's good programming style.




回答2:


The answer below is correct, I'd just like to add some more information as I was asked to in a comment elsewhere.

JPA uses caching to avoid database hits where possible, and where a database hit is still required, caching avoids the cost of rebuilding objects and allows maintaining Identity - ensuring you get back the same A instance when traversing A->B->A circular references.

When you persist an entity, you are placing it in the EntityManager cache as a managed entity - calling find on that EntityManager will return you the same exact instance you just passed in.

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

The persist call itself will not change anything within your entity (except possibly the ID if a sequence that allows preallocation to be used), so any null references will still be null.

Eventually the transaction commits and depending on your provider, entities can be cached in a second level cache. I'll assume you aren't using it for the sake of brevity; unless you force the EM to refresh this instance (flush first if its a new one!) or read it in a separate EntityManager, you will always get that same instance back with any null references.

If you refresh it or otherwise cause it to be reloaded, your JPA provider is required to set everything in the object as it is in the database, according to your mappings. Since null isn't a persistable state for a collection mapping, that means it will either eagerly fetch your references, or place proxies in there for lazy relationships, causing you to find an empty collection.



来源:https://stackoverflow.com/questions/46853396/why-entitys-uninitialized-collection-is-initialized-automatically-only-for-entit

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!