Hello All,
We've recently upgraded to 2.3.0 (from 1.2.2) and have encountered a nasty
bug where when loading a complicated cyclic object graph using a fetch
group, some relationships are not being populated under some circumstances.
Unfortunately, the graph really is quite complicated, and while I have a
set of test data where the problem can be reliably duplicated, I haven't
been able to make an integration test do the same yet. A simplified
version of the graph is that A is 1:M to B and A is 1:M to C, B is M:1 to A
(Bidirectional), and C is M:1 to A and C is also M:1 to B. The net effect
is a graph where cycles can exist, although in practice they generally
don't other than the bidirectional relationship between A and B.
Our failing case is interesting. When we load single instances of B (call
them B1 and B2) one at a time the load is always successful, with all of
the A, B, and C relationships populated - but only if loaded one at a time.
In other words, if we run entityManager.find(B1) or
entityManager.find(B2), then all is well.
An ascii art version of the graph traversals might look something like this:
B1 -> A1 -> C1 -> B3 -> A3 -> B3 -> A3
-> B3-1 -> A3
-> B3-2 -> A3
-> C1-2 -> ...
....
B2 -> A2 -> C2 -> A3 -> B3 -> A3
-> B3-1 -> A3
-> B3-2 -> A3
-> C2-2 -> ....
...
where B is M:1 to A, A is 1:M to C and A is 1:M to B.
Interestingly, while B1 and B2 are separate objects, they do share several
common objects in their graphs - call them A3 and B3 (as well as B3-1 and
B3-2). If we run a query that loads both B1 and B2 in the same query -
entityManager.find(B1 + B2), then one of the relationships from one of the
other B objects in the graph (call it B3) B3->A is null (not populated),
where B3->A should == A3. To clarify, B1.A1.C1.B3.getA() should equal A3,
and instead is null, and B2.A2.C2.A3.B3.getA() should also equal A3, but
instead is null.
Of course, the graph is being detached after load, so unfortunately lazy
loading the B3->A relationship is not possible.
When running through a debugger, it looks like what happens is that the A3
and B3-* instances are each being created more than one time. This seems
to make sense because the graph has cycles, relationships are loaded
recursively, and instances are not added to the transactional cache
(ManagedCache) until after they are fully initialized with all fields
loaded. Therefore new instances will not always be fully created when
they are needed again. As the call stack goes forward, the newest A and B
instances get fully initialized with all fields loaded. However, as the
call stack unwinds, the oldest A and B instances are the ones that
eventually win, and one of the B instances that wins is not fully
initialized and has a null field in it's A relationship (e.g. B3.getA() is
null, because B3's A was not ever set).
I'm currently tracing this through to try and determine exactly why the
outer (eldest) B3 isn't getting loaded with its A3, but while I do so, I
was wondering if anyone else has encountered a similar problem or has any
suggestions as to where I should focus my efforts.
My current thinking is that the multiple loading issue is OK and expected,
and that the problem is that the oldest B's aren't getting loaded with
their A's. But it is possible that the problem is that each individual
entity should only be initialized once, and that this is the root issue.
Comments would be welcome.
Thanks,
Jeff
We've recently upgraded to 2.3.0 (from 1.2.2) and have encountered a nasty
bug where when loading a complicated cyclic object graph using a fetch
group, some relationships are not being populated under some circumstances.
Unfortunately, the graph really is quite complicated, and while I have a
set of test data where the problem can be reliably duplicated, I haven't
been able to make an integration test do the same yet. A simplified
version of the graph is that A is 1:M to B and A is 1:M to C, B is M:1 to A
(Bidirectional), and C is M:1 to A and C is also M:1 to B. The net effect
is a graph where cycles can exist, although in practice they generally
don't other than the bidirectional relationship between A and B.
Our failing case is interesting. When we load single instances of B (call
them B1 and B2) one at a time the load is always successful, with all of
the A, B, and C relationships populated - but only if loaded one at a time.
In other words, if we run entityManager.find(B1) or
entityManager.find(B2), then all is well.
An ascii art version of the graph traversals might look something like this:
B1 -> A1 -> C1 -> B3 -> A3 -> B3 -> A3
-> B3-1 -> A3
-> B3-2 -> A3
-> C1-2 -> ...
....
B2 -> A2 -> C2 -> A3 -> B3 -> A3
-> B3-1 -> A3
-> B3-2 -> A3
-> C2-2 -> ....
...
where B is M:1 to A, A is 1:M to C and A is 1:M to B.
Interestingly, while B1 and B2 are separate objects, they do share several
common objects in their graphs - call them A3 and B3 (as well as B3-1 and
B3-2). If we run a query that loads both B1 and B2 in the same query -
entityManager.find(B1 + B2), then one of the relationships from one of the
other B objects in the graph (call it B3) B3->A is null (not populated),
where B3->A should == A3. To clarify, B1.A1.C1.B3.getA() should equal A3,
and instead is null, and B2.A2.C2.A3.B3.getA() should also equal A3, but
instead is null.
Of course, the graph is being detached after load, so unfortunately lazy
loading the B3->A relationship is not possible.
When running through a debugger, it looks like what happens is that the A3
and B3-* instances are each being created more than one time. This seems
to make sense because the graph has cycles, relationships are loaded
recursively, and instances are not added to the transactional cache
(ManagedCache) until after they are fully initialized with all fields
loaded. Therefore new instances will not always be fully created when
they are needed again. As the call stack goes forward, the newest A and B
instances get fully initialized with all fields loaded. However, as the
call stack unwinds, the oldest A and B instances are the ones that
eventually win, and one of the B instances that wins is not fully
initialized and has a null field in it's A relationship (e.g. B3.getA() is
null, because B3's A was not ever set).
I'm currently tracing this through to try and determine exactly why the
outer (eldest) B3 isn't getting loaded with its A3, but while I do so, I
was wondering if anyone else has encountered a similar problem or has any
suggestions as to where I should focus my efforts.
My current thinking is that the multiple loading issue is OK and expected,
and that the problem is that the oldest B's aren't getting loaded with
their A's. But it is possible that the problem is that each individual
entity should only be initialized once, and that this is the root issue.
Comments would be welcome.
Thanks,
Jeff