通过无知解决 LazyInitializationException

Posted

技术标签:

【中文标题】通过无知解决 LazyInitializationException【英文标题】:Solving LazyInitializationException via ignorance 【发布时间】:2015-12-02 02:55:51 【问题描述】:

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

但是是否可以简单地告诉 Hibernate 忽略该问题并假装集合为空?就我而言,之前不获取它只是意味着我不在乎。

这实际上是以下 Y 的 XY 问题:

我正在上课

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


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

并且想要处理两种请求:一种返回单个master 及其所有details,另一种返回masters 列表而没有details。结果由 Gson 转换为 JSON。

我已经尝试过session.clearsession.evict(master),但它们没有触及用于代替details 的代理。有效的是

 master.setDetails(nullOrSomeCollection)

这感觉相当hacky。我更喜欢“无知”,因为它通常适用于不知道代理的哪些部分。

编写一个 Gson TypeAdapter 忽略 AbstractPersistentCollectioninitialized=false 的实例可能是一种方法,但这将取决于 org.hibernate.collection.internal,这肯定不是一件好事。在TypeAdapter 中捕获异常听起来也好不到哪里去。

一些答案​​后更新

我的目标不是“获取数据加载而不是异常”,而是“如何获取null例外”

Dragan raises 一个有效的观点,忘记获取并返回错误的数据将比异常更糟糕。但是有一个简单的方法:

仅对集合执行此操作 永远不要为他们使用null 返回 null 而不是空集合作为未获取数据的指示

这样,结果永远不会被错误解释。如果我忘记取东西,响应将包含无效的null

【问题讨论】:

你读过我的解决方案***.com/questions/6354265/… @NassimMOUALEK 是的,我做到了。它还解决了另一个问题,即“如何加载数据而不是异常”,但我什么是“如何获取 null 而不是异常”。 我认为它可能会帮助您改变策略,您应该阅读此***.com/a/32039605/3252285 “如果我忘记获取某些东西,响应将包含无效的 null。” 但是您将如何区分故意未获取和忘记获取的集合?两者都返回null @DraganBozanovic 在服务器上根本不在乎,它只是不在乎:从它的 POV 来看,它是否获取了一个字段,它总是正确的,永远不会忘记。 +++ Java 客户端的对象看起来像我拒绝在主代码中使用的 DTO 并执行非空检查。这也应该在测试中完成。 +++ 当看到一个空值而不是一个集合时,一个 javascript 客户端会简单地崩溃。集成测试会显示这一点。 【参考方案1】:

您可以使用 Hibernate.isInitialized,它是 Hibernate 公共 API 的一部分。

所以,在TypeAdapter 中,您可以添加如下内容:

if ((value instanceof Collection) && !Hibernate.isInitialized(value)) 
   result = new ArrayList();

但是,在我看来,您的方法通常不是可行的方法。

“在我的情况下,之前不获取它只是意味着我不在乎。”

或者这意味着您忘记获取它并且现在您返回了错误的数据(比收到异常更糟糕;服务的消费者认为集合是空的,但事实并非如此)。

我不想提出“更好”的解决方案(这不是问题的主题,每种方法都有自己的优势),而是我在大多数用例中解决此类问题的方式(它是通常采用的方式)正在使用 DTO:只需定义一个代表服务响应的 DTO,将其填充到事务上下文中(那里没有 LazyInitializationExceptions)并将其提供给将其转换为服务响应的框架(json 、xml 等)。

【讨论】:

isInitialized 很好,但我必须将它应用于所有领域..​​....或者只是注册一个TypeAdapterFactory&lt;Collection&gt;!凉爽的! +++ 关于返回错误数据,我同意你的观点,但有一个补救措施(不完美:服务器返回无效答案,但客户端检测到问题)。当然,DTO 会起作用,但我认为它是 boilerplate(尽管在大型项目中可能是必要的邪恶)。 当然,不要对每个字段单独进行,这会破坏目的。注册一个自定义序列化程序。 我会在尝试时接受您的回答(可能是下周)。【参考方案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 中,当我问任何事情时都贪婪地抛出)。【参考方案3】:

我过去曾提出过一个类似的问题 (why dependent collection isn't evicted when parent entity is),它已经得出了一个答案,您可以尝试针对您的案例。

【讨论】:

我看不到如何应用它,但这只是我缺乏知识。我实际上相信Dragan's answer 解决了它,我只是还没有尝试过。同时,我相信我很快就会处理缓存问题,所以我正在为您的问题添加书签。 你说你“session.evict(master),但他们没有触及用于代替细节的代理”。我的问题是关于驱逐依赖集合(在您的情况下为“详细信息”)。 我想,驱逐是我的一个坏主意,但谢谢你,现在我明白了。【参考方案4】:

解决方案是使用查询而不是关联(一对多或多对多)。甚至 Hibernate 的原作者之一也说Collections are a feature 而不是最终目标。

在您的情况下,您可以更灵活地删除集合映射,并在数据访问层中需要它们时简单地获取关联的关系。

【讨论】:

【参考方案5】:

您可以为每个实体创建一个 Java 代理,这样每个方法都被一个 try/catch 块包围,当捕获到 LazyInitializationException 时返回 null

为此,您的所有实体都需要实现一个接口,并且您需要在整个程序中引用该接口(而不是实体类)。

如果您不能(或只是不想)使用接口,那么您可以尝试使用javassist 或cglib 构建动态代理,甚至手动构建,如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: 这可行,但增加了复杂性;考虑一下是否真的有必要。

【讨论】:

以上是关于通过无知解决 LazyInitializationException的主要内容,如果未能解决你的问题,请参考以下文章

内部接口——暴露我的无知

以下代码是不是违反持久性无知规则

我只是个无知的搬运工, 只能在这在这烦恼的夜晚做一点点搬运的工作

无知有知都要无畏

GET 和 POST请求的本质区别是什么?看完觉得自己太无知了...

究竟啥是“执着无知”?