关于LAZY加载的Hibernate 4 ClassCastException,而EAGER正常工作

时间:2012-01-07 13:45:01

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

我为地理区域(如大洲,国家,州等)提供了以下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,名称以及与其父级的关系(自引用)。请注意映射为​​parent的{​​{1}}地理区域关系。在数据库中,FK的FetchType.LAZYparent_id,使关系成为可选的。 <{1}}的默认值为NOT NULL,因此映射似乎是正确的。

子类定义了其他属性,实际上并不重要。数据库中的数据正确链接,因为可以通过JPQL列出地理区域而没有问题(@ManyToOne上的optional = true映射略有不同,请参见文本结尾):

Arena list with EAGER loading

每一行都是:

的实例
FetchType.EAGER

当运行列表查询时,父映射为LAZY,我得到以下异常:

parent

堆栈跟踪之前的println是:

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!

            ...
    }

    ...
}

println清楚地说父母是国家的一个例子,但它不能被投到国家?我不知道......

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

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

由于

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

3 个答案:

答案 0 :(得分:16)

延迟加载的问题是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时,您总是可以获得真实对象,但仍然可以从延迟加载中获益。你应该彻底测试一下,但是......

答案 1 :(得分:2)

罗伯特·彼得梅尔的回答很少。对于这种结构:

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

class State extends GeoArea {}

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

因此,您可以为每个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是关于这个问题的一些更详细的解释。

答案 2 :(得分:0)

您正在使用JBoss模块类加载器。

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

因此,您必须检查application modules或使用标准类加载的其他应用程序服务器。