OneToMany association with Lazy Loading and Cascade.ALL


#1

Hi.

I’m observing a strange behaviour that I’m not able to explain in Hibernate 5.2.17

I have an entity with several oneToMany collections in it.
All these collections are traversable in a bi-directional way
All of them are marked as LazyLoading and Cascade.ALL (we introduced the latter because it helps graphql generation).

When a detached entity of this type is being saved - without changing or accessing any of the associated collections - , hibernate triggers a load (that is fine) but it includes also the first (eg: the entity with the name that is first in lexical ordering) collection in the generated sql.
I do really don’t want to access an associated entity if it was not changed in the code and I expect Lazy Loading to prevent this.
Why Hibernate has this behaviour?
Is there a way to avoid this still using the cascade.all annotation?

Many thanks in advance


#2

In order to answer your question, you need to add the entities, the data access code and the SQL logs generated by Hibernate.


#4

Try to replicate it with this test case, as it’s not clear why that join is generated from the mappings alone.


#5

Thanks Vlad. I’ll replicate with a testcase and update the post


#6

Hi.
Here you can find and example tescase (some help with this also from a colleague)

The testcase used is the JPA one.

Looking at the Hibernate logs it’s possible to see that the ORM is trying to load information for the first set of entities, not the second.

Thanks


#7

Based on your test case:

Parent parent = new Parent();
parent.setId(1L);
//when merging hibernate core calls a load that retrieves also data from the first child (Child and not Nephew) - see hibernate logs
entityManager.merge(parent);

The SELECT is expected since that’show merge works. For a detailed explanation check out these articles:


#8

Thanks for the further explanation.
Anyway I appreciate that being called a merge the persistence engine first checks for any change in the underlying table so it performs a select.
But why it includes in a select an associated table (child) even if no change was made on it in the code and it’s lazy loaded. And why nephew is not included in the select even if is defined exactly as child (lazy loaded with cascade.all)?


#9

First, the mapping is wrong since it should be like this:

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Child> children = new HashSet<>();

@OneToMany(mappedBy = "uncle", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Nephew> nephews = new HashSet<>();

Notice the mappedBy attribute needed by this bidirectional association.

Anyway, this does not affect the OUTER JOIN.

But why it includes in a select an associated table (child) even if no change was made on it in the code and it’s lazy loaded.

Because it needs to load the entire graph in order to detect changes that need to be propagated via Cascade. If you don’t want the JOIN, you either have to call persist or remove the Cascade.

And why nephew is not included in the select even if is defined exactly as child (lazy loaded with cascade.all)?

The executed query is this one:

select
    parent0_.id as id1_2_1_,
    parent0_.last_name as last_nam2_2_1_,
    parent0_.name as name3_2_1_,
    children1_.parent_id as parent_i4_0_3_,
    children1_.Id as Id1_0_3_,
    children1_.Id as Id1_0_0_,
    children1_.last_name as last_nam2_0_0_,
    children1_.name as name3_0_0_,
    children1_.parent_id as parent_i4_0_0_ 
from
    parent parent0_ 
left outer join
    Child children1_ 
        on parent0_.id=children1_.parent_id 
where
    parent0_.id=?

Now, I debugged the CascadeEntityJoinWalker class and it seems it restrict the OUTER JOIN count to avoid Cartesian Products. If the nephew association is needed, it will trigger a secondary query.


#10

Thanks a lot for all this explanation!
Very useful


#11

Thanks.
Why does it need to add that JOIN if the value of Set<Child> children is null? In that case there’s nothing to cascade.


#12

Why does it need to add that JOIN if the value of Set<Child> children is null? In that case there’s nothing to cascade.

First of all, using null for collections is a bad idea.

So, instead of:

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Child> children;

You should always have:

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Child> children = new HashSet<>();

Second, even if you set the Set to null, that would only be taken into account for transient entities. Hibernate will have to fetch that association when the association is accessed, so it’s either a Proxy or a fully-initialized collection, but never null.