LazyInitializationException after JOIN FETCH



我们有一个Spring Boot/JPA应用程序,它有一个实体,指定从不同度量单位到不同度量单位的转换。这种转换特定于我们使用的每个产品,因此该实体有一个具有3个属性的组合键:FromUnitOfMeasure、ToUnitOfMeasur和product。

@Getter
@Setter
@EqualsAndHashCode(of = "unitOfMeasureConversionByMaterialCompositeKey")
@NoArgsConstructor
@RequiredArgsConstructor
@Entity
public class UnitOfMeasureConversionByMaterial {
@EmbeddedId
@NonNull
private UnitOfMeasureConversionByMaterialCompositeKey unitOfMeasureConversionByMaterialCompositeKey;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
@EqualsAndHashCode
public static class UnitOfMeasureConversionByMaterialCompositeKey implements Serializable {
@ManyToOne(fetch = FetchType.LAZY)
@NonNull
private Product product;
@ManyToOne(fetch = FetchType.LAZY)
@NonNull
private UnitOfMeasure fromUnitOfMeasure;
@ManyToOne(fetch = FetchType.LAZY)
@NonNull
private UnitOfMeasure toUnitOfMeasure;
}

private Float conversionValue;

UnitOfMeasure类:

@Getter
@Setter
@ToString(of="id")
@EqualsAndHashCode(of = "id")
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class UnitOfMeasure implements Serializable {
@Id
private String id;
private String description;

public UnitOfMeasure(String id) {
this.id = id;
}

}

整个逻辑在异步线程上运行,因此没有可用的打开会话(相当于spring.jpa.open-in-view=false(

@ManyToOne字段是Lazy字段,因此我们在单元转换存储库中使用JOIN FETCH查询来获取转换和@ManyToPone字段(因此遵循https://vladmihalcea.com/the-best-way-to-handle-the-lazyinitializationexception/)

@Query("SELECT cup FROM UnitOfMeasureConversionByMaterial cbm "
+ "LEFT JOIN FETCH cbm.unitOfMeasureConversionByMaterialCompositeKey.product p "
+ "LEFT JOIN FETCH cbm.unitOfMeasureConversionByMaterialCompositeKey.fromUnitOfMeasure fum "
+ "LEFT JOIN FETCH cbm.unitOfMeasureConversionByMaterialCompositeKey.toUnitOfMeasure tum")
List<UnitOfMeasureConversionByMaterial> customFindAllJoinProductsAndUom();

然后,应用程序继续迭代所有转换实体,并填充一个映射,该映射按产品对转换进行索引,包括UnitOfMeasure和targetUnitOfMeasur:

protected Map<Product,Map<UnitOfMeasure,Map<UnitOfMeasure,Float>>> indexedConversionMap = new HashMap<>();

这是我在映射中插入值的代码部分。该代码在前24.500次转换中成功运行,但随后在填充映射的最终级别(为给定的ToUnitOfMeasure设置浮点转换值(时引发"org.ibernate.LazyInitializationException:无法初始化proxy[test.domain.UnitOfMeasure#kg]-no-Session"异常。

for (UnitOfMeasureConversionByMaterial unitOfMeasureConversionByMaterial : conversionsExtractedWithJoinFetchQuery) {
Product product = unitOfMeasureConversionByMaterial.getProduct();
UnitOfMeasure fromUnitOfMeasure = unitOfMeasureConversionByMaterial.getFromUnitOfMeasure ();
UnitOfMeasure toUnitOfMeasure = unitOfMeasureConversionByMaterial.getToUnitOfMeasure ();
Float conversion = unitOfMeasureConversionByMaterial.getConversionValue();            

if (!unitOfMeasureProjection.indexedConversionMap.containsKey(product)) {
unitOfMeasureProjection.indexedConversionMap.put(product, new HashMap<>());
}
if (!unitOfMeasureProjection.indexedConversionMap.get(product).containsKey(fromUnitOfMeasure)) {
unitOfMeasureProjection.indexedConversionMap.get(product).put(fromUnitOfMeasure, new HashMap<>());
}

unitOfMeasureProjection.indexedConversionMap.get(product).get(fromUnitOfMeasure).put(
toUnitOfMeasure, conversion); // ERROR happens here
}

错误消息为:

org.hibernate.LazyInitializationException: could not initialize proxy [test.domain.UnitOfMeasure#kg] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:175)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:315)
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
at test.domain.unitofmeasureconversionbymaterial.UnitOfMeasure$HibernateProxy$2LNVM7uD.hashCode(Unknown Source)
at java.base/java.util.HashMap.hash(HashMap.java:338)
at java.base/java.util.HashMap.put(HashMap.java:610)
at test.UnitOfMeasureProjectionFactory.getUnitOfMeasureProjectionWithConversions(UnitOfMeasureProjectionFactory.java:104)
at test.ConversionService.executeConversion(ConversionService.java:326)
at test.executeConversion$$FastClassBySpringCGLIB$$a7815e11.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
at test.ConversionService$$EnhancerBySpringCGLIB$$77fac61.executeConversion(<generated>)
at test.job.executeAsJob(ConversionJob.java:36)
at test.job.executeAsJob(ConversionJob.java:20)
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:833)

一些附加信息:

  • 我已经尝试在UnitOfMeasure实体中取消对Equals和HashCode的绑定
    • 它们工作正常,仅依赖于String-id属性
  • spring.jpa.open-in-view=true不是一个选项,因为我们正在启动一个不同的线程。性能也是这个应用程序的关键,所以我们不能指望延迟获取
  • 我们的服务中的总体@Transactional注释也不是一个选项,因为数十万条记录正在批量保存。我们有内存约束(应用程序和数据库(,单个事务操作会违反这些约束

我不明白的是:

  • 为什么代码在前24.500次转换中运行时没有问题,然后才显示错误
  • 为什么即使我们使用JOIN FETCH提取所有@ManyToOne属性的转换(这些属性又有简单的String-id(,Hibernate也会尝试访问数据库来获取信息

可能出了什么问题?关于如何调试这个异常,有什么想法吗?

编辑1:

以下是自定义查询的SQL:

select
unitofmeas0_.product_id as product_4_31_0_,
unitofmeas0_.to_unit_of_measure_id as unit_of_3_31_0_,
unitofmeas0_.from_unit_of_measure_id as unit_of_2_31_0_,
product1_.id as id1_97_1_,
unitofmeas2_.id as id1_129_2_,
unitofmeas3_.id as id1_129_3_,
unitofmeas0_.conversion_value as conversi1_31_0_,
product1_.active as active2_97_1_,
product1_.descontinuation_date as desconti3_97_1_,
product1_.introduction_date as introduc4_97_1_,
product1_.description as descript5_97_1_,
product1_.ean as ean6_97_1_,
product1_.operational_model as operatio7_97_1_,
product1_.ncm as ncm8_97_1_,
product1_.standard_unit_of_measure_id as standard9_97_1_,
product1_.transfer_unit_of_measure_id as transfer10_97_1_,
product1_.sales_unit_of_measure_id as sales_un11_97_1_,
unitofmeas2_.description as descript2_129_2_,
unitofmeas3_.description as descript2_129_3_ 
from
unit_of_measure_conversion_by_material unitofmeas0_ 
left outer join
product product1_ 
on unitofmeas0_.product_id=product_.id 
left outer join
unit_of_measure unitofmeas2_ 
on unitofmeas0_.from_unit_of_measure_id=unitofmeas2_.id 
left outer join
unit_of_measure unitofmeas3_ 
on unitofmeas0_.to_unit_of_measure_id=unitofmeas3_.id

我们在Product实体中与UnitOfMeasure有其他关系,但我们认为这不是问题所在,因为我们试图用下面的Query联接FETCH Product字段,但错误仍然存在。

@Query("SELECT cup FROM UnitOfMeasureConversionByMaterial cbm "
+ "LEFT JOIN FETCH cbm.unitOfMeasureConversionByMaterialCompositeKey.product p "
+ "LEFT JOIN FETCH cbm.unitOfMeasureConversionByMaterialCompositeKey.fromUnitOfMeasure fum "
+ "LEFT JOIN FETCH cbm.unitOfMeasureConversionByMaterialCompositeKey.toUnitOfMeasure tum "
+ "LEFT JOIN FETCH p.standardUnitOfMeasure stum "
+ "LEFT JOIN FETCH p.salesUnitOfMeasure saum "
+ "LEFT JOIN FETCH p.transferUnitOfMeasure tum")
List<UnitOfMeasureConversionByMaterial> customFindAllJoinProductsAndUom();

SQL:

select
unitofmeas0_.product_id as product_4_31_0_,
unitofmeas0_.to_unit_of_measure_id as unit_of_3_31_0_,
unitofmeas0_.from_unit_of_measure_id as unit_of_2_31_0_,
product1_.id as id1_97_1_,
unitofmeas2_.id as id1_129_2_,
unitofmeas3_.id as id1_129_3_,
unitofmeas4_.id as id1_129_4_,
unitofmeas5_.id as id1_129_5_,
unitofmeas6_.id as id1_129_6_,
unitofmeas0_.conversion_value as conversi1_31_0_,
product1_.active as active2_97_1_,
product1_.descontinuation_date as desconti3_97_1_,
product1_.introduction_date as introduc4_97_1_,
product1_.description as descript5_97_1_,
product1_.ean as ean6_97_1_,
product1_.operational_model as operatio7_97_1_,
product1_.ncm as ncm8_97_1_,
product1_.standard_unit_of_measure_id as standard9_97_1_,
product1_.transfer_unit_of_measure_id as transfer10_97_1_,
product1_.sales_unit_of_measure_id as sales_un11_97_1_,
unitofmeas2_.description as descript2_129_2_,
unitofmeas3_.description as descript2_129_3_,
unitofmeas4_.description as descript2_129_4_,
unitofmeas5_.description as descript2_129_5_,
unitofmeas6_.description as descript2_129_6_ 
from
unit_of_measure_conversion_by_material unitofmeas0_ 
left outer join
product product1_ 
on unitofmeas0_.product_id=product1_.id 
left outer join
unit_of_measure unitofmeas2_ 
on unitofmeas0_.from_unit_of_measure_id=unitofmeas2_.id 
left outer join
unit_of_measure unitofmeas3_ 
on unitofmeas0_.to_unit_of_measure_id=unitofmeas3_.id 
left outer join
unit_of_measure unitofmeas4_ 
on product1_.standard_unit_of_measure_id=unitofmeas4_.id 
left outer join
unit_of_measure unitofmeas5_ 
on product1_.sales_unit_of_measure_id=unitofmeas5_.id 
left outer join
unit_of_measure unitofmeas6_ 
on product1_.transfer_unit_of_measure_id_id=unitofmeas6_.id

编辑2:

我们还尝试以以下方式分离方法调用以分析问题:

for (UnitOfMeasureConversionByMaterial unitOfMeasureConversionByMaterial : conversionsExtractedWithJoinFetchQuery) {
unitOfMeasureConversionByMaterial.getFromUnitOfMeasure().getId();
unitOfMeasureConversionByMaterial.getFromUnitOfMeasure().hashCode();
unitOfMeasureConversionByMaterial.getToUnitOfMeasure().getId();
unitOfMeasureConversionByMaterial.getToUnitOfMeasure().hashCode();
}

一切都以预期的方式进行,直到我们尝试将";hashCode(("方法在";toUnitOfMeasure";在第五行。";unitOfMeasureConversionByMaterial.getToUnitOfMeasure(("部分工作正常,但一旦我们尝试调用";hashCode(("方法,则代码生成LazyInitializationException。它甚至不执行该方法的第一行。

为什么代码在前24.500次转换中运行时没有问题,然后错误才显示在上

HashMap不仅使用hashCode()方法,还使用equals()方法。当一个bucket有多个值时,equals()用于从映射中获取值。我没有研究HashMap的实际实现,例如,在增加conatiner的内存时,它可以使用equals()

此外,例如,持久对象的toString()方法也可以在某个地方使用。例如,在记录错误期间。

请记住,Hibernate不研究持久类的方法。因此,如果调用任何类似toString()的方法,Hibernate会尝试加载所有惰性关联,因为它们可以在方法中使用。

如何调试

  1. 为代码的所有部分临时打开Hibernate会话
  2. 启用SQL日志记录
  3. 尝试在24500次转换后设置断点并调试代码请记住,调试器可以调用toString()方法并导致加载惰性关联

一些注意事项

重写持久类的hashCode()equals()方法是个坏主意。使用龙目是一个非常非常糟糕的主意。

使用持久类作为映射的键也是一个基本想法。

并非所有情况下都可以使用复合键。