Spring FetchType.Lazy结合Projection加载含Eager实体报错解决方案咨询
数据库数据量达数百万级,已将所有配置改为FetchType.Lazy懒加载,并使用Spring Projection进行数据查询。在Projection中添加无Eager关联的Lugar实体时,Postman能正常返回数据;但添加包含多个Eager关联的ArticulosV3实体时,出现序列化错误。
推测报错原因是ArticulosV3中的Eager关联导致的,当前Projection代码如下:
@JsonIgnoreProperties("decoratedClass") public interface UbicacionesLugaresProjection { Integer getId(); int getTipolugar(); Lugar getiDlugar(); boolean getPordefecto(); ArticulosV3 getArticuloOneway(); }
可正常加载的关联配置:
@JoinColumn(name = "ID_lugar", referencedColumnName = "ID") @ManyToOne(optional = false) @JsonView(JsonViews.Summary.class) private Lugar iDlugar;
(Lugar无Eager关联)
无法加载的关联配置:
@JoinColumn(name = "articulo_oneway", referencedColumnName = "ID") @NotFound(action = NotFoundAction.IGNORE) @ManyToOne(optional = true) @JsonView(JsonViews.Summary.class) private ArticulosV3 articuloOneway;
(ArticulosV3包含多个Eager关联,且无法移除这些Eager配置)
报错信息:
2024-04-25 17:09:19 [http-nio-8080-exec-2] WARN o.s.w.s.m.s.DefaultHandlerExceptionResolver - Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: java.util.ArrayList[0]->$Proxy349["articuloOneway"]->com.clickrent.backoffice.ws.entities.ArticulosV3["articulosUbicacion"]->org.hibernate.collection.internal.PersistentBag[0]->com.clickrent.backoffice.ws.entities.ArticulosUbicacion["idUbicaciones"]->com.clickrent.backoffice.ws.entities.Ubicaciones_$$_jvst1a7_9e["handler"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: java.util.ArrayList[0]->$Proxy349["articuloOneway"]->com.clickrent.backoffice.ws.entities.ArticulosV3["articulosUbicacion"]->org.hibernate.collection.internal.PersistentBag[0]->com.clickrent.backoffice.ws.entities.ArticulosUbicacion["idUbicaciones"]->com.clickrent.backoffice.ws.entities.Ubicaciones_$$_jvst1a7_9e["handler"])
咨询:能否在Projection中嵌套另一个Projection来加载ArticulosV3,避开其Eager关联?或者有其他可行解决方案?
1. 嵌套Projection(最优方案)
完全可以通过嵌套Projection解决问题,核心是用自定义Projection替代直接返回ArticulosV3实体,只选择业务需要的字段,避免触发其内部的Eager关联。
实现步骤:
- 先创建
ArticulosV3对应的Projection接口,仅定义需要的getter方法,排除会触发Eager关联的字段:
public interface ArticulosV3Projection { Integer getId(); String getNombre(); // 替换为你实际需要的业务字段 // 仅保留必要字段,不要添加关联其他Eager实体的方法 }
- 修改原
UbicacionesLugaresProjection,将返回ArticulosV3的方法改为返回新的Projection:
@JsonIgnoreProperties("decoratedClass") public interface UbicacionesLugaresProjection { Integer getId(); int getTipolugar(); Lugar getiDlugar(); boolean getPordefecto(); ArticulosV3Projection getArticuloOneway(); // 替换为Projection类型 }
Spring Data JPA会自动只查询指定字段,不会加载ArticulosV3内部的Eager关联,从根源避免序列化时的懒加载代理问题。
2. 其他备选方案
方案A:禁用Jackson的FAIL_ON_EMPTY_BEANS特性
在Spring配置中添加以下代码,让Jackson忽略无法序列化的空bean,但这只是临时规避,可能返回不必要的代理字段,不推荐长期使用:
@Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); return mapper; } }
方案B:用@JsonIgnore屏蔽关联字段
在ArticulosV3实体中,对引发问题的Eager关联字段添加@JsonIgnore,但需注意如果该实体在其他业务场景需要这些字段,会产生影响,需谨慎使用:
@JsonIgnore @ManyToOne(fetch = FetchType.EAGER) private OtroEntity otroEntity; // 示例关联字段
方案C:自定义DTO+手动映射
如果Projection不够灵活,可以创建自定义DTO类,在Repository中查询指定字段后手动映射,完全控制返回的数据结构:
// 主DTO类 public class UbicacionesLugaresDTO { private Integer id; private int tipolugar; private Lugar lugar; private boolean pordefecto; private ArticulosV3DTO articuloOneway; // getter、setter、构造方法 } // 关联DTO类 public class ArticulosV3DTO { private Integer id; private String nombre; // 其他需要的字段 }
在Repository中使用@Query查询指定字段,再通过Stream或工具类映射到DTO:
@Query("SELECT new com.example.dto.UbicacionesLugaresDTO(u.id, u.tipolugar, u.iDlugar, u.pordefecto, new com.example.dto.ArticulosV3DTO(a.id, a.nombre)) FROM Ubicaciones u LEFT JOIN u.articuloOneway a") List<UbicacionesLugaresDTO> findAllWithProjection();
内容的提问来源于stack exchange,提问作者user15558580

