0

I have the following design:

enter image description here

One sports game has two scores (one per team) and each score has multiple player stats, one per jersey number. Both relationships are mapped as Map. This is just multi-level aggregation.

Game entity:

@Entity
@Table(name = "\"Games\"")
@NamedQuery(name = Game.FIND_ALL, query = "SELECT ga FROM Game ga")
@NamedEntityGraph(name = Game.FETCH_SCORES, attributeNodes = {@NamedAttributeNode("scores")})
@NamedEntityGraph(name = Game.FETCH_SCORES_AND_PLAYER_STATS,
                  attributeNodes = {@NamedAttributeNode(value = "scores", subgraph = Score.FETCH_PLAYER_STATS)},
                  subgraphs = {@NamedSubgraph(name = Score.FETCH_PLAYER_STATS, attributeNodes = @NamedAttributeNode("playerStats"))}
)
public class Game implements Serializable
{
    private static final long serialVersionUID = 1L;

    public static final String FIND_ALL = "Game.findAll";
    public static final String FETCH_SCORES = "Game.fetchScores";
    public static final String FETCH_SCORES_AND_PLAYER_STATS = "Game.fetchScoresAndPlayerStats";

    @Id
    @Column
    private Integer id;

    @Basic(optional = false)
    @Column(name = "scheduled_tipoff")
    private LocalDateTime scheduledTipoff;

    @OneToMany(mappedBy = "game")
    // @MapKey(name = "home")
    @MapKeyColumn(name = "is_home", insertable = false, updatable = false)
    private Map<Boolean, Score> scores;

    ...
}

Score entity (PK is "home or away of a game", so a Boolean home in the PK is enough):

@Entity
@Table(name = "\"Scores\"")
@IdClass(ScoreId.class)
public class Score implements Serializable
{
    private static final long serialVersionUID = 1L;

    public static final String FETCH_PLAYER_STATS = "Score.fetchPlayerStats";

    @Id
    @Column(name = "game_id")
    private Integer gameId;

    @Id
    @Column(name = "is_home")
    private Boolean home;

    @Basic(optional = false)
    @Column(name = "roster_id")
    private Integer rosterId;

    @Basic
    @Column(name = "final_score")
    private Integer finalScore;

    @ManyToOne(optional = false, fetch = FetchType.EAGER)
    @JoinColumn(name = "game_id", insertable = false, updatable = false)
    private Game game;

    @OneToMany(mappedBy = "score")
    @MapKeyColumn(name = "jersey_nbr")
    private Map<Integer, PlayerStat> playerStats;

    ...
}

ScoreId PK class:

public class ScoreId implements Serializable
{
    private static final long serialVersionUID = 1L;

    private Integer game;

    private Boolean home;

    ...
}

PlayerStat entity:

@Entity
@Table(name = "\"PlayerStats\"")
@IdClass(PlayerStatId.class)
public class PlayerStat implements Serializable
{
    private static final long serialVersionUID = 1L;

    @Id
    @Column(name = "game_id")
    private Integer gameId;

    @Id
    @Column(name = "is_home")
    private Boolean home;

    @Id
    @Column(name = "player_id")
    private Integer playerId;

    @Basic(optional = false)
    @Column(name = "jersey_nbr", insertable = false, updatable = false)
    private Integer jerseyNbr;

    @ManyToOne(optional = false, fetch = FetchType.EAGER)
    @JoinColumn(name = "game_id", referencedColumnName = "game_id", insertable = false, updatable = false)
    @JoinColumn(name = "is_home", referencedColumnName = "is_home", insertable = false, updatable = false)
    private Score score;
    
    ...
}

PlayerStatId:

public class PlayerStatId implements Serializable
{
    private static final long serialVersionUID = 1L;

    private Integer gameId;

    private Boolean home;

    private Integer playerId;

    ...
}

The following query works:

// fetch only scores: OK
games = gameService.findByNamedQuery( Game.FIND_BY_GROUP, Game.FETCH_SCORES, with( "roundId", group.getRoundId() ).and( "groupCode", group.getCode() ).map() );

Taking it one level further to fetch player stats attached to each score:

// fetch player stats: not OK
games = gameService.findByNamedQuery( Game.FIND_BY_GROUP, Game.FETCH_SCORES_AND_PLAYER_STATS, with( "roundId", group.getRoundId() ).and( "groupCode", group.getCode() ).map() );

...EclipseLink fails with:

Caused by: javax.persistence.PersistenceException: java.lang.ClassCastException: org.eclipse.persistence.indirection.IndirectMap cannot be cast to org.eclipse.persistence.queries.FetchGroupTracker
    at org.eclipse.persistence.internal.jpa.QueryImpl.getResultList(QueryImpl.java:493)
    at net.bbstatstest.i303.GameService.findAllWithScores(GameService.java:40)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.jboss.as.ee.component.ManagedReferenceMethodInterceptor.processInvocation(ManagedReferenceMethodInterceptor.java:52)
    at org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at org.jboss.invocation.InterceptorContext$Invocation.proceed(InterceptorContext.java:509)
    at org.jboss.as.weld.ejb.DelegatingInterceptorInvocationContext.proceed(DelegatingInterceptorInvocationContext.java:92)
    at org.jboss.weld.interceptor.proxy.WeldInvocationContextImpl.interceptorChainCompleted(WeldInvocationContextImpl.java:107)
    at org.jboss.weld.interceptor.proxy.WeldInvocationContextImpl.proceed(WeldInvocationContextImpl.java:126)
    at com.arjuna.ats.jta.cdi.transactional.TransactionalInterceptorBase.invokeInCallerTx(TransactionalInterceptorBase.java:203)
    at com.arjuna.ats.jta.cdi.transactional.TransactionalInterceptorSupports.doIntercept(TransactionalInterceptorSupports.java:55)
    at com.arjuna.ats.jta.cdi.transactional.TransactionalInterceptorBase.intercept(TransactionalInterceptorBase.java:86)
    at com.arjuna.ats.jta.cdi.transactional.TransactionalInterceptorSupports.intercept(TransactionalInterceptorSupports.java:47)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.jboss.weld.interceptor.reader.SimpleInterceptorInvocation$SimpleMethodInvocation.invoke(SimpleInterceptorInvocation.java:73)
    at org.jboss.weld.interceptor.proxy.WeldInvocationContextImpl.invokeNext(WeldInvocationContextImpl.java:92)
    at org.jboss.weld.interceptor.proxy.WeldInvocationContextImpl.proceed(WeldInvocationContextImpl.java:124)
    at org.jboss.weld.bean.InterceptorImpl.intercept(InterceptorImpl.java:105)
    at org.jboss.as.weld.ejb.DelegatingInterceptorInvocationContext.proceed(DelegatingInterceptorInvocationContext.java:82)
    at org.jboss.as.weld.interceptors.EjbComponentInterceptorSupport.delegateInterception(EjbComponentInterceptorSupport.java:60)
    at org.jboss.as.weld.interceptors.Jsr299BindingsInterceptor.delegateInterception(Jsr299BindingsInterceptor.java:77)
    at org.jboss.as.weld.interceptors.Jsr299BindingsInterceptor.doMethodInterception(Jsr299BindingsInterceptor.java:89)
    at org.jboss.as.weld.interceptors.Jsr299BindingsInterceptor.processInvocation(Jsr299BindingsInterceptor.java:102)
    at org.jboss.as.ee.component.interceptors.UserInterceptorFactory$1.processInvocation(UserInterceptorFactory.java:63)
    at org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at org.jboss.as.ejb3.component.invocationmetrics.ExecutionTimeInterceptor.processInvocation(ExecutionTimeInterceptor.java:43)
    at org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at org.jboss.as.jpa.interceptor.SBInvocationInterceptor.processInvocation(SBInvocationInterceptor.java:47)
    at org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at org.jboss.as.ee.concurrent.ConcurrentContextInterceptor.processInvocation(ConcurrentContextInterceptor.java:45)
    at org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at org.jboss.invocation.InitialInterceptor.processInvocation(InitialInterceptor.java:40)
    at org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at org.jboss.invocation.ChainedInterceptor.processInvocation(ChainedInterceptor.java:53)
    at org.jboss.as.ee.component.interceptors.ComponentDispatcherInterceptor.processInvocation(ComponentDispatcherInterceptor.java:52)
    at org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at org.jboss.as.ejb3.component.pool.PooledInstanceInterceptor.processInvocation(PooledInstanceInterceptor.java:51)
    at org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at org.jboss.as.ejb3.component.interceptors.AdditionalSetupInterceptor.processInvocation(AdditionalSetupInterceptor.java:54)
    at org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at org.jboss.as.ejb3.tx.CMTTxInterceptor.invokeInOurTx(CMTTxInterceptor.java:252)
    ... 133 more
Caused by: java.lang.ClassCastException: org.eclipse.persistence.indirection.IndirectMap cannot be cast to org.eclipse.persistence.queries.FetchGroupTracker
    at org.eclipse.persistence.descriptors.FetchGroupManager.getObjectFetchGroup(FetchGroupManager.java:697)
    at org.eclipse.persistence.internal.descriptors.ObjectBuilder.load(ObjectBuilder.java:788)
    at org.eclipse.persistence.internal.sessions.AbstractSession.load(AbstractSession.java:5335)
    at org.eclipse.persistence.queries.ObjectLevelReadQuery.executeDatabaseQuery(ObjectLevelReadQuery.java:1249)
    at org.eclipse.persistence.queries.DatabaseQuery.execute(DatabaseQuery.java:911)
    at org.eclipse.persistence.queries.ObjectLevelReadQuery.execute(ObjectLevelReadQuery.java:1191)
    at org.eclipse.persistence.queries.ReadAllQuery.execute(ReadAllQuery.java:485)
    at org.eclipse.persistence.internal.sessions.AbstractSession.internalExecuteQuery(AbstractSession.java:3356)
    at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1898)
    at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1880)
    at org.eclipse.persistence.internal.indirection.QueryBasedValueHolder.instantiate(QueryBasedValueHolder.java:135)
    at org.eclipse.persistence.internal.indirection.QueryBasedValueHolder.instantiate(QueryBasedValueHolder.java:122)
    at org.eclipse.persistence.internal.indirection.DatabaseValueHolder.getValue(DatabaseValueHolder.java:97)
    at org.eclipse.persistence.internal.indirection.UnitOfWorkValueHolder.instantiateImpl(UnitOfWorkValueHolder.java:175)
    at org.eclipse.persistence.internal.indirection.UnitOfWorkValueHolder.instantiate(UnitOfWorkValueHolder.java:238)
    at org.eclipse.persistence.internal.indirection.DatabaseValueHolder.getValue(DatabaseValueHolder.java:97)
    at org.eclipse.persistence.indirection.IndirectMap.buildDelegate(IndirectMap.java:129)
    at org.eclipse.persistence.indirection.IndirectMap.getDelegate(IndirectMap.java:416)
    at org.eclipse.persistence.indirection.IndirectMap.size(IndirectMap.java:952)
    at org.eclipse.persistence.internal.queries.MapContainerPolicy.sizeFor(MapContainerPolicy.java:848)
    at org.eclipse.persistence.internal.indirection.TransparentIndirectionPolicy.instantiateObject(TransparentIndirectionPolicy.java:392)
    at org.eclipse.persistence.mappings.ForeignReferenceMapping.instantiateAttribute(ForeignReferenceMapping.java:1136)
    at org.eclipse.persistence.mappings.CollectionMapping.load(CollectionMapping.java:1404)
    at org.eclipse.persistence.internal.descriptors.ObjectBuilder.load(ObjectBuilder.java:813)
    at org.eclipse.persistence.internal.sessions.AbstractSession.load(AbstractSession.java:5335)
    at org.eclipse.persistence.internal.sessions.AbstractSession.load(AbstractSession.java:5327)
    at org.eclipse.persistence.queries.ObjectLevelReadQuery.executeDatabaseQuery(ObjectLevelReadQuery.java:1249)
    at org.eclipse.persistence.queries.DatabaseQuery.execute(DatabaseQuery.java:911)
    at org.eclipse.persistence.queries.ObjectLevelReadQuery.execute(ObjectLevelReadQuery.java:1191)
    at org.eclipse.persistence.queries.ReadAllQuery.execute(ReadAllQuery.java:485)
    at org.eclipse.persistence.queries.ObjectLevelReadQuery.executeInUnitOfWork(ObjectLevelReadQuery.java:1279)
    at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.internalExecuteQuery(UnitOfWorkImpl.java:2983)
    at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1898)
    at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1880)
    at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1845)
    at org.eclipse.persistence.internal.jpa.QueryImpl.executeReadQuery(QueryImpl.java:262)
    at org.eclipse.persistence.internal.jpa.QueryImpl.getResultList(QueryImpl.java:482)
    ... 179 more

QUESTION:

What's wrong? There must be something going wrong with subgraph = Score.FETCH_PLAYER_STATS.

Is this a bug in EclipseLink? Mappings wrong? Query + graph wrong?

EclipseLink version is 2.7.7, see stacktrace.

PS: This works in Hibernate BTW.

Kawu
  • 13,647
  • 34
  • 123
  • 195
  • Odd, as error comes from ((FetchGroupTracker)domainObject)._persistence_getFetchGroup(); which I don't see how it could return something other than a FetchGroupTracker instance.Try the latest (hail marry type response), but debugging would be required - can you get the woven classes and put a break point in Score's _persistence_getFetchGroup() to see where it is coming from - I suspect a bug but can't tell without stepping through it myself. – Chris Jun 11 '20 at 16:57
  • Hmmm. I believe I could possibly get the woven classes. I'm not a great debugger user. Eclipse BTW. Hmm. How do I get the woven classes? – Kawu Jun 11 '20 at 17:17
  • I tried the latest EclipseLink version and it still throws the same exception. I will try to provide compile a more minimum example... – Kawu Jun 11 '20 at 23:04
  • Last I tried it, I just used static weaving, decompiled the .class file, and just use that in debugging as the 'source'. A break point in EclipseLink classes though might mean you don't need it, as you can inspect the byte code for your model classes without the woven java files. – Chris Jun 12 '20 at 14:33
  • I added a test scenario here: https://github.com/kawoolutions/primefaces-test/tree/master/src/main/java/org/eclipselink/test/ccemapfetchgrouptracker However, the I was only able to get EclipseLink to run with **static weaving** (Maven plugin, see pom.xml). The exception doesn't occurr in this sample test case. I suspect it has something to do with dynamic weaving? Do you happen to have a link to a working EclipseLink test project, e.g. on Github?? This error is very annoying... – Kawu Jun 14 '20 at 17:13
  • I seem to have gotton it to go with dynamic weaving now. I get the following exception: `Exception Description: You must define a fetch group manager at descriptor (org.eclipselink.test.ccemapfetchgrouptracker.entity.Game) in order to set a fetch group on the query (Game.findAll) Query: ReadAllQuery(name="Game.findAll" referenceClass=Game sql="SELECT ID, ATTENDANCE, official_nbr, scheduled_tipoff FROM "Games"")`. Much like unanswered question https://stackoverflow.com/questions/47338358/entitygraph-you-must-define-a-fetch-group-manager-at-descriptor-in-order-to-se – Kawu Jun 15 '20 at 11:55
  • So static weaving 'works'? IMO that is the preferred solution. It certainly indicates that weaving is a problem in your environment though, and seems related to the "You must define a fetch group manager at descriptor" error message. Your 1:1 relationships are marked eager, but I'm guessing are eager even if you marked them as lazy as lazy requires weaving. Any luck debugging with a breakpoint where you get that ClassCastException? Your debugger might let you just break when it is encountered and you can see the 'domainObject' in question. – Chris Jun 15 '20 at 17:26
  • Looks like this is already an issue in EclipseLink, see my answer. – Kawu Dec 06 '20 at 15:29

1 Answers1

0

It's a bug.

See https://bugs.eclipse.org/bugs/show_bug.cgi?id=495892

It boils down to EclipseLink being unable to handle a Map type using entity sub graphs.

When using @MapKey, the CCE is Hashtable to FetchgroupTracker, if you use @MapKeyColumn the CCE is IndirectMap to FetchGroupTracker.

Kawu
  • 13,647
  • 34
  • 123
  • 195
  • Same issue from 2016: https://stackoverflow.com/questions/37746308/jpa-entitygraph-and-map – Kawu Dec 06 '20 at 15:26