You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何连续两次独立事务触发LazyInitializationException异常?

问题:拆分事务后JPA懒加载抛出LazyInitializationException

我原本有一段将JPA实体转为图顶点、JPA关联转为图边的代码,在小型数据库运行正常,但大型数据库会触发OOM(内存溢出)。于是我尝试拆分事务做流式处理,结果抛出了懒加载异常。

原代码(正常运行但大库OOM)

public GraphTraversalSource modelGraphFromMetamodelGraph(org.jgrapht.Graph<MetamodelVertex, FieldEdge<MetamodelVertex>> entityMetamodelGraph) {
    log.info("Creating model vertex");
    GraphTraversalSource gTS = this.graphTraversalSource.getGraph();
    TransactionTemplate tpl = new TransactionTemplate(sourcePlatformTxManager);
    tpl.setReadOnly(true);
    tpl.executeWithoutResult(status -> {
        createVertices(entityMetamodelGraph, gTS);
        createEdges(entityMetamodelGraph, gTS);
    });
    return gTS;
}

逻辑说明:单事务内完成顶点和边的创建,事务结束后实体被detach,但边创建在同一事务中,懒加载集合可正常初始化。

修改后代码(拆分事务但抛异常)

public GraphTraversalSource modelGraphFromMetamodelGraph(org.jgrapht.Graph<MetamodelVertex, FieldEdge<MetamodelVertex>> entityMetamodelGraph) {
    log.info("Creating model vertex");
    GraphTraversalSource gTS = this.graphTraversalSource.getGraph();
    createVertices(entityMetamodelGraph, gTS);
    createEdges(entityMetamodelGraph, gTS);
    return gTS;
}

private void createVertices(Graph<MetamodelVertex, FieldEdge<MetamodelVertex>> entityMetamodelGraph, GraphTraversalSource graphTraversalSource) {
    log.info("Creating model Vertex");
    TransactionTemplate tpl = new TransactionTemplate(sourcePlatformTxManager);
    tpl.setReadOnly(true);
    tpl.executeWithoutResult(status ->
        createVertices(entityMetamodelGraph.vertexSet(), graphTraversalSource);
    );
}

void createEdges(Graph<MetamodelVertex, FieldEdge<MetamodelVertex>> entityMetamodelGraph, GraphTraversalSource graphTraversalSource) {
    log.info("Creating model edges");
    TransactionTemplate tpl = new TransactionTemplate(sourcePlatformTxManager);
    tpl.setReadOnly(true);
    tpl.executeWithoutResult(status ->
        modelGraphEdgeBuilder.createEdges(graphTraversalSource, entityMetamodelGraph);
    );
}

逻辑说明:把顶点和边的创建拆成两个独立事务,试图通过流式处理降低内存占用。

抛出的异常信息

[ERROR] net.osgiliath.FakerProcessingIT.givenFedGraphWhenEntityProcessorAndSequenceProcessorIsCalledThenTargetDatabaseIsPopulatedExcludingCyclicPathAndFieldsAreTransformed -- Time elapsed: 0.079 s <<< ERROR!
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: net.osgiliath.datamigrator.sample.domain.JhiUser.jhiAuthorities: could not initialize proxy - no Session
        at org.hibernate.collection.spi.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:634)
        at org.hibernate.collection.spi.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:217)
        at org.hibernate.collection.spi.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:613)
        at org.hibernate.collection.spi.AbstractPersistentCollection.read(AbstractPersistentCollection.java:136)
        at org.hibernate.collection.spi.PersistentSet.iterator(PersistentSet.java:166)
        at java.base/java.util.Spliterators$IteratorSpliterator.estimateSize(Spliterators.java:1959)
        at java.base/java.util.Spliterator.getExactSizeIfKnown(Spliterator.java:414)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:508)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
        at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
        at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
        at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:276)
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
        at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
        at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
        at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
        at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
        at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
        at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:276)
        at org.apache.tinkerpop.gremlin.process.traversal.Traversal.forEachRemaining(Traversal.java:278)
        at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
        at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
        at java.base/java.util.stream.ReferencePipeline.reduce(ReferencePipeline.java:667)
        at net.osgiliath.migrator.core.graph.TinkerpopModelGraphEdgeBuilder.createEdges(TinkerpopModelGraphEdgeBuilder.java:68)

代码意图说明

假设有实体A和B,A包含指向B的OneToMany关联x:

  • 原方法:单事务内执行select * from A和select * from B,将实例包装为顶点,此时x因懒加载未被加载;事务结束后实体被detach。
  • 修改后方法:先在独立事务创建顶点,再在另一个独立事务创建边。创建边时遍历A的实例,尝试访问关联x获取B实例以创建边,期望新事务能重新关联A实体并加载懒加载集合,但未达到预期。

原因分析

拆分事务后,创建顶点的事务结束时,所有JPA实体都被detach(脱离持久化上下文)。后续创建边的事务启动后,这些游离状态的实体并不会自动重新关联到新的持久化上下文——Hibernate不会主动将游离实体重新附着,除非显式调用EntityManager.merge()或重新查询实体。

当在创建边的阶段尝试访问JhiUser.jhiAuthorities这个懒加载集合时,该集合的代理对象仍持有旧事务的上下文引用,但旧事务已关闭,无法初始化集合,因此抛出LazyInitializationException。


修复方案

以下是几种可行的解决思路:

1. 显式重新关联实体到当前事务

在创建边的事务内,遍历实体时通过EntityManager.merge()将游离状态的实体重新附着到当前持久化上下文,或直接重新查询实体:

// 在createEdges方法的事务内处理每个实体
EntityManager em = ...; // 获取当前事务的EntityManager
A detachedA = ...; // 游离状态的A实体
A managedA = em.merge(detachedA); // 重新关联到当前上下文
// 此时访问managedA.getX()可正常初始化懒加载集合

注意:merge()返回托管状态的实体,必须使用该返回值访问关联集合,原游离实体的状态不会改变。

2. 创建顶点时预加载所有需要的关联

如果业务允许,在创建顶点阶段通过JOIN FETCH或EntityManager.fetch()预加载后续创建边需要的所有关联集合,这样即使实体被detach,集合数据已加载到内存,后续无需访问数据库:

// 修改查询A的语句,预加载x关联
TypedQuery<A> query = em.createQuery("SELECT a FROM A a JOIN FETCH a.x", A.class);
List<A> entities = query.getResultList();
// 此时x集合已加载,后续detach后仍可访问

注意:若关联数据量极大,需配合分页或流式查询控制内存,避免回到OOM问题。

3. 更小粒度的事务拆分(流式处理)

不要一次性处理所有顶点或边,分批次处理,每批数据在独立事务中完成顶点创建+边创建,既控制内存占用,又保证每个批次内实体处于托管状态:

public GraphTraversalSource modelGraphFromMetamodelGraph(...) {
    GraphTraversalSource gTS = ...;
    // 按批次遍历实体类型
    for (MetamodelVertex vertex : entityMetamodelGraph.vertexSet()) {
        TransactionTemplate tpl = new TransactionTemplate(sourcePlatformTxManager);
        tpl.setReadOnly(true);
        tpl.executeWithoutResult(status -> {
            // 批量创建当前类型的顶点
            createBatchVertices(vertex, gTS);
            // 批量创建当前类型关联的边
            createBatchEdges(vertex, gTS);
        });
    }
    return gTS;
}

这种方式每个批次的事务只处理部分数据,内存压力小,且同一事务内创建顶点和边,实体始终处于托管状态,懒加载集合可正常初始化。

4. 开启Hibernate的enable_lazy_load_no_trans(不推荐)

可在Hibernate配置中开启hibernate.enable_lazy_load_no_trans,允许在事务外初始化懒加载集合。但该方式会导致每个懒加载操作开启临时事务,性能低下,易引发N+1查询问题,仅作为临时应急方案。


内容的提问来源于Stack Exchange,提问作者Tcharl

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 11:14:51