12

我有以下地理区域(如大陆、国家、州等)的 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映射为​​的地理区域关系FetchType.LAZY。在数据库中,FKparent_id使NOT NULL关系成为可选的。的默认@ManyToOne值为optional = true,因此映射似乎是正确的。

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

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 明确表示父级是 State 的一个实例,但不能强制转换为 State?我不知道...

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

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

谢谢

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

4

3 回答 3

19

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

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

阅读此问答,其中展示了如何使用标记界面HibernateProxy将代理转换为真实对象。在这种情况下,另一种选择是使用急切加载。

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

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

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

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

可以自动化吗?

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

于 2012-01-07T15:02:21.953 回答
3

很少有东西可以扩展 Robert Petermeier 的答案。对于这个结构:

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

class State extends GeoArea {}

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

因此,您可以为每次instanceof使用或类型转换打开代理,但这不是我的选择(我在一个大系统中做了很多懒惰的关联以提高数据库性能 - 100% 确定系统是否仍然工作我' d 需要在我能看到类型转换或instanceof) 的任何地方添加展开。但是您可以选择使用 字节码检测来自动化它。

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

}   

这里有一些关于这个问题更详细的解释。

于 2017-05-28T12:11:59.803 回答
0

您正在使用JBossModule ClassLoaders。

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

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

于 2016-01-29T09:04:50.287 回答