我们有一个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会尝试加载所有惰性关联,因为它们可以在方法中使用。
如何调试
- 为代码的所有部分临时打开Hibernate会话
- 启用SQL日志记录
- 尝试在24500次转换后设置断点并调试代码请记住,调试器可以调用
toString()
方法并导致加载惰性关联
一些注意事项
重写持久类的hashCode()
和equals()
方法是个坏主意。使用龙目是一个非常非常糟糕的主意。
使用持久类作为映射的键也是一个基本想法。
并非所有情况下都可以使用复合键。