通过忽略解决LazyInitializationException问题

16

这里有无数问题,如何通过急切取回、保持事务打开、打开另一个事务、OpenEntityManagerInViewFilter等方式解决“无法初始化代理”的问题。

但是,是否可以简单地告诉Hibernate忽略这个问题,并假装集合为空?在我的情况下,不在获取它之前意味着我不关心它。

实际上,这是一个带有以下Y的XY问题:

我有像这样的类

class Detail {
    @ManyToOne(optional=false) Master master;
    ...
}

class Master {
    @OneToMany(mappedBy="master") List<Detail> details;
    ...
}

我想提供两种请求:一种返回单个具有其所有细节的master,另一种返回没有detailsmaster列表。结果由Gson转换为JSON。

我尝试了session.clearsession.evict(master),但它们不会影响代替details使用的代理。有效的方法是:

 master.setDetails(nullOrSomeCollection)

这种方法感觉有点狡猾。我更喜欢“忽略”,因为它可以普遍适用,而不需要知道代理了哪些部分。

编写一个 Gson TypeAdapter,忽略具有 initialized=falseAbstractPersistentCollection 实例可能是一种方式,但这将依赖于org.hibernate.collection.internal,这肯定不是什么好事情。在 TypeAdapter 中捕获异常似乎也不太好。

在得到一些答案之后更新

我的目标不是“获取已加载的数据而不是异常”,而是“如何获取< strong > null 而不是异常”

Dragan 提出了 一个合理的观点,即忘记获取并返回错误数据比抛出异常更糟糕。但是有一个简单的解决方法:

  • 只对集合执行此操作
  • 永远不要使用null (空指针)
  • 返回null而不是空集合,以表示未获取的数据

这样,结果就永远不会被错误解释。如果我忘记获取某些内容,响应将包含null,这是无效的。


1
你看过我的解决方案吗?https://dev59.com/E2w15IYBdhLWcg3w3fjN#32046337 - Nassim MOUALEK
@NassimMOUALEK 是的,我做到了。这也解决了另一个问题,即“如何获取加载的数据而不是异常”,但我想要的是“如何获取null而不是异常”。 - maaartinus
@DraganBozanovic 在服务器上完全不需要,它只关心:从它的视角来看,无论它是否获取字段,它总是正确的,永远不会被遗忘。 +++ Java客户端将具有类似于DTO的对象,我拒绝在主代码中使用,并进行非空检查。这也应该在测试中完成。 +++ Javascript客户端只需在看到null而不是集合时就会崩溃。集成测试将显示此内容。 - maaartinus
@DraganBozanovic 所以,我基本上通过消除DTO并将获取/非获取数据的单一真实来源来简化main代码。在我看来,这是很好的:更小、更快、更简单,没有要保持同步的两个东西。+++同时,它必须经过测试,因此test代码需要一些DTO或其他描述应该获取什么的信息。这也可以测试是否获取了不必要的内容,否则当在主要代码中构建DTO时,就会丢失这些信息。我还没有尝试过,你觉得呢? - maaartinus
好的,这样就消除了潜在的“null”歧义。基本上,通过仅初始化所需内容,您正在服务器端创建一种“隐形”的DTO,其中仅包含实体的初始化状态。如果是这样的话,那么我个人认为您的方法很有趣。 - Dragan Bozanovic
显示剩余4条评论
5个回答

5
你可以使用Hibernate公共API的一部分,即Hibernate.isInitialized
因此,在TypeAdapter中,你可以添加类似以下内容的代码:
if ((value instanceof Collection) && !Hibernate.isInitialized(value)) {
   result = new ArrayList();
}

然而,在我谦虚的意见中,您的整体方法并不是正确的方式。

“在我的情况下,不获取它仅仅意味着我不关心。”

或者这意味着您忘记获取它,现在返回错误的数据(比抛出异常更糟糕;服务的使用者认为集合为空,但实际上不是)。

我不想提出“更好”的解决方案(这不是问题的主题,每种方法都有其优势),但我在大多数情况下解决这些问题的方式(也是常用的方式之一)是使用DTO:简单地定义一个DTO来表示服务的响应,在事务上下文中填充它(没有LazyInitializationException),并将其提供给将其转换为服务响应(json、xml等)的框架。


isInitialized 这个名字还不错,但是我得把它应用到所有的字段上...或者只需注册一个 TypeAdapterFactory<Collection>!太棒了! +++ 关于返回错误数据,我同意你的看法,但有一种解决方法(并不完美:服务器返回无效答案,但客户端检测到问题)。当然,DTO 可以起作用,但我认为它是一个样板文件(尽管在大型项目中可能是必要的恶魔)。 - maaartinus
当然,不要为每个字段单独执行此操作,这将失去其意义。注册一个自定义序列化程序。 - Dragan Bozanovic
我会在尝试之后接受你的答案(可能是下周)。 - maaartinus

2

我不知道如何应用它,但这只是我的知识缺乏。实际上,我相信Dragan的答案解决了这个问题,只是我还没有尝试过。同时,我相信我很快就会处理缓存,所以我会将你的问题加入书签。 - maaartinus
你说过你“session.evict(master)”,但是它们并没有触及代替“details”的代理。我的问题是关于清除依赖集合(在你的情况下是“details”)。 - mindas
我猜,驱逐是我的一个坏主意,但谢谢你,现在我明白了。 - maaartinus

2
你可以尝试以下类似的解决方案:
创建一个名为 LazyLoader 的接口。
@FunctionalInterface // Java 8
public interface LazyLoader<T> {
    void load(T t);
}

在你的服务中,

public class Service {
    List<Master> getWithDetails(LazyLoader<Master> loader) {
        // Code to get masterList from session
        for(Master master:masterList) {
            loader.load(master);
        }        
    }
}

并像下面这样调用此服务

Service.getWithDetails(new LazyLoader<Master>() {
    public void load(Master master) {
        for(Detail detail:master.getDetails()) {
            detail.getId(); // This will load detail
        }
    }
});

在Java 8中,由于Lambda是"单一抽象方法"(SAM),因此可以使用它。

Service.getWithDetails((master) -> {
    for(Detail detail:master.getDetails()) {
        detail.getId(); // This will load detail
    }
});

您可以使用上述解决方案,使用session.clearsession.evict(master)

看起来你正在解决“如何获取详细信息”的问题,但我的问题是“如何在没有详细信息和代理抛出的情况下获取Master”。无论是session.clear()还是session.evict(master)似乎都无法解决这个问题(代理魔鬼仍然在details中贪婪地等待着我询问它时抛出异常)。 - maaartinus

1
解决方法是使用查询而不是关联(一对多或多对多)。即使 Hibernate 的原始作者之一也说过,集合只是一个特性,而不是最终目标。
在您的情况下,您可以更好地灵活地移除集合映射,并在需要时在数据访问层中简单地获取相关联的关系。

1
你可以为每个实体创建一个Java代理,这样每个方法都将被try/catch块包围,在捕获到LazyInitializationException时返回null。
为了使其起作用,所有实体都需要实现一个接口,并且在整个程序中引用此接口(而不是实体类)。
如果您不能(或者只是不想)使用接口,则可以尝试使用javassistcglib构建动态代理,甚至可以像this article中所解释的那样手动构建。
如果您使用普通的Java代理,这里是一个草图:
public static <T> T ignoringLazyInitialization(
    final Object entity, 
    final Class<T> entityInterface) {

    return (T) Proxy.newProxyInstance(
        entityInterface.getClassLoader(),
        new Class[] { entityInterface },
        new InvocationHandler() {

            @Override
            public Object invoke(
                Object proxy, 
                Method method, 
                Object[] args) 
                throws Throwable {

                try {
                    return method.invoke(entity, args);
                } catch (InvocationTargetException e) {
                    Throwable cause = e.getTargetException();
                    if (cause instanceof LazyInitializationException) {
                        return null;
                    }
                    throw cause;
                }
            }
        });
}

所以,如果你有一个实体A,如下所示:
public interface A {

    // getters & setters and other methods DEFINITIONS

}

在其实施中:

public class AImpl implements A {

    // getters & setters and other methods IMPLEMENTATIONS

}

假设您有一个对实体类的引用(由Hibernate返回),则可以按以下方式创建代理:
AImpl entityAImpl = ...; // some query, load, etc

A entityA = ignoringLazyInitialization(entityAImpl, A.class);

注1:您还需要代理由Hibernate返回的集合(留给读者练习);)

注2:理想情况下,您应该在DAO或某种外观中执行所有这些代理操作,以便实体的用户对此一无所知。

注3:这绝不是最佳选择,因为它会为每次访问未初始化字段创建一个堆栈跟踪。

注4:这样做可行,但会增加复杂性;请考虑是否真的有必要。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接