Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hibernate 4 ClassCastException on LAZY loading while EAGER works fine

I have the following JOINED inheritance root entity for geographic areas (like continents, countries, states etc.):

@Entity
@Table(name = "GeoAreas")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class GeoArea implements Serializable
{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    protected Integer id;

    @Column
    protected String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id", referencedColumnName = "id")
    protected GeoArea parent;

    ...
}

As you can see a geo area has a simple auto-ID as PK, a name, and a relationship to its parent (self reference). Please pay attention to the parent geo area relationship mapped as FetchType.LAZY. In the DB the FK's parent_id is NOT NULL making the relationship optional. The default for @ManyToOne is optional = true, so the mappings seem to be correct.

The sub classes define additional properties and aren't of interest really. The data in the DB is linked correctly as the geo areas can be listed without problems via JPQL (with a slightly different FetchType.EAGER mapping on parent, see end of text):

Arena list with EAGER loading

Each line is an instance of:

public class ArenaListViewLine
{
    private final Integer arenaId;
    private final String arenaName;
    private final String arenaLabel;

    private final Boolean hasPostAddress;

    ...

    public ArenaListViewLine(Arena arena, Boolean hasPostAddress)
    {
        // init fields
        ...

        List<String> continentNames = new ArrayList<String>();
        List<String> countryNames = new ArrayList<String>();
        List<String> regionNames = new ArrayList<String>();
        List<String> stateNames = new ArrayList<String>();
        List<String> districtNames = new ArrayList<String>();
        List<String> clubShorthands = new ArrayList<String>();

        // add each geo area to list whose club is a user of an arena (an arena has several usages)
        // in border areas of states two clubs from different states might use an arena (rare!)
        // this potentially adds several states to an entry in the arena list (non in DB yet)
        for ( Usage us : usages )
        {
            Club cl = us.getClub();

            // a club is located in one district at all times (required, NOT NULL)
            District di = cl.getDistrict();

            System.out.println("arena = " + arenaName + ": using club's district parent = " + di.getParent());

            State st = (State)di.getParent(); // ClassCastException here!

            ...
    }

    ...
}

When running the list query with parent mapped as LAZY, I get the following exception:

...
Caused by: org.hibernate.QueryException: could not instantiate class [com.kawoolutions.bbstats.view.ArenaListViewLine] from tuple
    at org.hibernate.transform.AliasToBeanConstructorResultTransformer.transformTuple(AliasToBeanConstructorResultTransformer.java:57) [hibernate-core-4.0.0.Final.jar:4.0.0.Final]
    at org.hibernate.hql.internal.HolderInstantiator.instantiate(HolderInstantiator.java:95) [hibernate-core-4.0.0.Final.jar:4.0.0.Final]
    at org.hibernate.loader.hql.QueryLoader.getResultList(QueryLoader.java:438) [hibernate-core-4.0.0.Final.jar:4.0.0.Final]
    at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2279) [hibernate-core-4.0.0.Final.jar:4.0.0.Final]
    at org.hibernate.loader.Loader.list(Loader.java:2274) [hibernate-core-4.0.0.Final.jar:4.0.0.Final]
    at org.hibernate.loader.hql.QueryLoader.list(QueryLoader.java:470) [hibernate-core-4.0.0.Final.jar:4.0.0.Final]
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.list(QueryTranslatorImpl.java:355) [hibernate-core-4.0.0.Final.jar:4.0.0.Final]
    at org.hibernate.engine.query.spi.HQLQueryPlan.performList(HQLQueryPlan.java:196) [hibernate-core-4.0.0.Final.jar:4.0.0.Final]
    at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1115) [hibernate-core-4.0.0.Final.jar:4.0.0.Final]
    at org.hibernate.internal.QueryImpl.list(QueryImpl.java:101) [hibernate-core-4.0.0.Final.jar:4.0.0.Final]
    at org.hibernate.ejb.QueryImpl.getResultList(QueryImpl.java:252) [hibernate-entitymanager-4.0.0.Final.jar:4.0.0.Final]
    ... 96 more
Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) [:1.7.0_02]
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source) [:1.7.0_02]
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source) [:1.7.0_02]
    at java.lang.reflect.Constructor.newInstance(Unknown Source) [:1.7.0_02]
    at org.hibernate.transform.AliasToBeanConstructorResultTransformer.transformTuple(AliasToBeanConstructorResultTransformer.java:54) [hibernate-core-4.0.0.Final.jar:4.0.0.Final]
    ... 106 more
Caused by: java.lang.ClassCastException: com.kawoolutions.bbstats.model.GeoArea_$$_javassist_273 cannot be cast to com.kawoolutions.bbstats.model.State
    at com.kawoolutions.bbstats.view.ArenaListViewLine.<init>(ArenaListViewLine.java:92) [classes:]
    ... 111 more

The println prior to the stack trace is:

13:57:47,245 INFO  [stdout] (http--127.0.0.1-8080-4) arena = Joachim-Schumann-Schule: using club's district parent = com.kawoolutions.bbstats.model.State@2b60693e[id=258,name=Hesse,isoCode=HE,country=com.kawoolutions.bbstats.model.Country@17f9976b[id=88,name=Germany,isoCode=DE,isoNbr=276,dialCode=<null>]]

The println clearly says the parent is an instance of State, but it cannot be cast to State? I have no idea...

When changing the FetchType of GeoArea.parent to EAGER everything works fine (see image above).

What am I doing wrong? What's wrong with the LAZY hint?

Thanks

PS: I'm using Hibernate 4.0.0.Final, the mappings are all standard JPA, server is JBoss AS 7.

like image 920
Kawu Avatar asked Jan 07 '12 13:01

Kawu


1 Answers

The problem with lazy loading is that Hibernate will dynamically generate proxies for objects that are lazily loaded. While at first this seems like a good idea to implement lazy loading, a developer has to be aware of the fact that an object can be a Hibernate Proxy. I think it's a good example of a leaky abstraction. In your case, the return value of the call di.getParent() is such a proxy object, generated throuhg the javassist library which is used by Hibernate.

Such Hibernate proxies will cause problems in connection with casts, instanceof and calls to equals() and hashCode() if you haven't implemented those methods for your entities.

Read this question and answer which shows how to use the marker interface HibernateProxy in order to convert a proxy to the real object. The other option is to go with eager loading in this case.

By the way, please think about ditching that "Hungarian notation" ;-)

Edited to address the questions from the comment:

Is there a portable (JPA) way to achieve this?

Not that I know of. I'd abstract the conversion logic from the other QA away behind an interface and provide an alternative noop implementation Using JBoss 7 and CDI you could switch to that one without even recompiling.

Can it be automated?

It might be possible using AspectJ. You could try to write an around advice for all calls to getters on your entities which return other entities. The advice will check if the underlying field is a Hibernate Proxy, and if so, apply the conversion, then return the result from the conversion. Thus, you would always get the real object when the getter is called but could still benefit from lazy loading. You should test this thoroughly, though...

like image 199
Robert Petermeier Avatar answered Sep 19 '22 11:09

Robert Petermeier