为何连续两次独立事务触发LazyInitializationException异常?
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

