【问题标题】:Hibernate 4 ClassCastException on LAZY loading while EAGER works fineHibernate 4 ClassCastException 延迟加载,而 EAGER 工作正常
【发布时间】:2012-02-04 21:47:20
【问题描述】:

我有以下地理区域(如大陆、国家、州等)的 JOINED 继承根实体:

@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;

    ...
}

如您所见,地理区域有一个简单的自动 ID(如 PK)、一个名称以及与其父级的关系(自我引用)。请注意映射为​​FetchType.LAZYparent 地理区域关系。在数据库中,FK 的parent_idNOT NULL,使得关系是可选的。 @ManyToOne 的默认值为optional = true,因此映射似乎是正确的。

子类定义了额外的属性,实际上并不感兴趣。数据库中的数据已正确链接,因为可以通过 JPQL 毫无问题地列出地理区域(parent 上的 FetchType.EAGER 映射略有不同,见文末):

每一行都是一个实例:

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!

            ...
    }

    ...
}

在父映射为 LAZY 的情况下运行列表查询时,出现以下异常:

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

堆栈跟踪之前的 println 是:

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

println 明明说parent是State的一个实例,但是不能强制转换成State?我不知道...

将 GeoArea.parent 的 FetchType 更改为 EAGER 时,一切正常(见上图)。

我做错了什么? LAZY 提示有什么问题?

谢谢

PS:我用的是Hibernate 4.0.0.Final,映射都是标准的JPA,服务器是JBoss AS 7。

【问题讨论】:

    标签: hibernate jpa lazy-loading classcastexception hibernate-4.x


    【解决方案1】:

    延迟加载的问题在于 Hibernate 会为延迟加载的对象动态生成代理。虽然起初这似乎是实现延迟加载的好主意,但开发人员必须意识到对象可以是 Hibernate 代理这一事实。我认为这是泄漏抽象的一个很好的例子。在您的情况下,调用 di.getParent() 的返回值就是这样一个代理对象,通过 Hibernate 使用的 javassist 库生成。

    如果您尚未为实体实现这些方法,此类 Hibernate 代理将导致与强制转换、instanceof 以及对 equals()hashCode() 的调用相关的问题。

    阅读this question and answer,它展示了如何使用标记接口HibernateProxy 将代理转换为真实对象。在这种情况下,另一种选择是使用预先加载。

    顺便说一句,请考虑放弃“匈牙利符号”;-)

    编辑以解决评论中的问题:

    是否有可移植的 (JPA) 方法来实现这一点?

    我不知道。我会将转换逻辑从其他 QA 中抽象出来,放在一个接口后面,并提供一个替代的 noop 实现使用 JBoss 7 和 CDI,您甚至可以切换到那个,甚至无需重新编译。

    可以自动化吗?

    使用 AspectJ 可能是可能的。您可以尝试为返回其他实体的实体上的所有 getter 调用编写 around 建议。该通知将检查底层字段是否为 Hibernate 代理,如果是,则应用转换,然后返回转换的结果。因此,当调用 getter 时,您总是会得到真实的对象,但仍然可以从延迟加载中受益。不过,您应该彻底测试一下...

    【讨论】:

    • 是否有可移植 (JPA) 方法来实现这一目标?可以自动化吗?
    【解决方案2】:

    罗伯特·彼得迈尔的回答几乎没有什么可以扩展的。对于这个结构:

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

    因为GeoArea.parent 是一个惰性代理,所以创建这个代理Hibernate 不知道加载到这个字段中的真正类是什么。它只知道它将属于GeoArea 子类。因此,它创建了一个 GeoArea 类的 javaassist 代理,该类永远不会转换为 StateGeoArea 的任何不同子类型。

    因此,您可以为每个 instanceof 使用或类型转换解包代理,但这不是我的选择(我在大型系统中做了很多懒惰的关联以提高数据库性能 - 100% 确定是否系统仍然可以工作,我需要在可以看到类型转换或instanceof 的任何地方添加展开。但是您可以选择使用 bytecode instrumentation

    最后我找到了一个无缝的解决方案来避免字节码检测 - 在庞大的系统中,我们仍然不知道在添加这个检测之后什么不再工作,以及在手动添加解包代码之后 :) 你可以应用在 getter 中手动解包代理的简单技巧。这是我的例子:

    @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;
        }
    
    }   
    

    here是关于这个问题的一些更详细的解释。

    【讨论】:

      【解决方案3】:

      您正在将 JBoss 与 Module ClassLoaders 一起使用。

      ClassCastException 使用代理的主要原因是 代理(由Hibernate 生成)正在使用状态类的不同实例作为调用者(客户端)。在同一个 Java JM 中必须有两个状态类实例! JBoss 处理模块和模块之间的隐式/显式导入/导出依赖关系。

      所以您必须检查您的application modules 或使用其他具有标准类加载的应用程序服务器。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-01-19
        • 2020-01-29
        • 2013-10-03
        • 2012-05-21
        • 1970-01-01
        • 2019-09-01
        相关资源
        最近更新 更多