问题
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