Hibernate 4 ClassCastException on LAZY loading while EAGER works fine

别说谁变了你拦得住时间么 提交于 2019-12-09 16:24:17

问题


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):

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.


回答1:


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...




回答2:


Few things to extend Robert Petermeier answer. For this construction:

class GeoArea {
    @ManyToOne(fetch = FetchType.LAZY)
    protected GeoArea parent;
}

class State extends GeoArea {}

Because GeoArea.parent is a lazy proxy, creating this proxy Hibernate doesn't know what will be the real class loaded into this field. It only knowns that it will be of GeoArea subclass. So it creates a javaassist proxy to GeoArea class that will never be castable to State or any different subtype of GeoArea.

So, you can unwrap proxy for each instanceof usage or type cast, however it wasn't an option for me (I made lazy a lot associations in a big system to improve database performance - to be 100% sure if the system still works I'd need to add unwrapping everywhere I can see the type cast or instanceof). But you have an option to automate it using bytecode instrumentation.

Finally I've found a seamless solution to avoid bytecode instrumentation - in the huge system we still won't know what is not working anymore after adding this instrumentation, as well as after adding unwrapping code manually :) You can apply a simple trick to unwrap the proxy manually in the getter. Here is my example:

@MappedSuperclass
public class BaseEntity {

    public BaseEntity getThis() {
        return this;
    }

}

@Entity
public class B extends BaseEntity {

    @ManyToOne(fetch = FetchType.LAZY)
    protected A source;

    public A getSource() {
        return this.source!=null ? (A) this.source.getThis() : null;
    }

    public void setSource(A source) {
        this.source = source;
    }

}   

And here are some more detailed explanations about the problem.




回答3:


You are using JBoss with Module ClassLoaders.

The main reason for a ClassCastException with proxies is that the proxy (generated by Hibernate) is using a different instance of the state class as the caller (client). There must be two instances of your state class in the same Java JM! JBoss handles that with modules and implicit/explicit import/export dependencies between modules.

So you have to check your application modules or use an other application server with standard class loading.



来源:https://stackoverflow.com/questions/8770124/hibernate-4-classcastexception-on-lazy-loading-while-eager-works-fine

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