Hibernate LazyInitializationException问题排查与解决(序列化场景)
问题描述
在自定义MachineGroupSerializer序列化器中,尝试通过ObjectMapper获取MachineGroup实体的多对多关联字段versions时,持续抛出以下异常:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.altair.autoTester.entities.machinegroup.MachineGroup.versions, could not initialize proxy - no Session
相关代码如下:
MachineGroup 实体
@Entity @Table(name="machine_groups") @Getter @Setter @AllArgsConstructor(access = AccessLevel.PUBLIC) @NoArgsConstructor public class MachineGroup { @Id @GeneratedValue(strategy= GenerationType.AUTO, generator = "machine_groups_seq") @SequenceGenerator(name = "machine_groups_seq", allocationSize = 1, initialValue = 2) @Column(name = "id") private long id; @ManyToMany(cascade = CascadeType.MERGE) @JoinTable(name = "machine_groups_to_versions", joinColumns = @JoinColumn(name = "machine_group_id"), inverseJoinColumns = @JoinColumn(name = "version_id")) @JsonManagedReference(value="machineGroups-versions") private Set<Version> versions = new HashSet<>(); @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinTable(name = "machine_groups_to_tr_types", joinColumns = @JoinColumn(name = "machine_group_id"), inverseJoinColumns = @JoinColumn(name = "tr_type_id")) private Set<TrType> trTypes = new HashSet<>(); }
Version 实体
@Entity @Table(name="versions") @Getter @Setter public class Version { @Id @GeneratedValue(strategy = GenerationType.AUTO, generator = "versions_seq") @SequenceGenerator(name = "versions_seq", allocationSize = 1) @Column(name = "id") private long id; @Column(name = "name", unique = true) private String name; @Column(name = "creation_time") private Date creationTime; @Column(name = "exe_file") @Lob private Blob exeFile; @JsonBackReference(value="machineGroups-versions") @ManyToMany(mappedBy = "versions", cascade = CascadeType.MERGE) private Set<MachineGroup> machineGroups = new HashSet<>(); public Version(){}; public Version(String name) { this.name = name; } }
MachineGroupSerializer 序列化器
public class MachineGroupSerializer extends StdSerializer<MachineGroup> { public MachineGroupSerializer() { this(null); } public MachineGroupSerializer(Class<MachineGroup> t) { super(t); } @Transactional @Override public void serialize(MachineGroup machineGroup, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeStartObject(); jsonGenerator.writeNumberField("id",machineGroup.getId()); jsonGenerator.writeObjectField("versions", machineGroup.getVersions().stream().map(Version::getId).collect(Collectors.toSet())); jsonGenerator.writeObjectField("trTypes", machineGroup.getTrTypes().stream().map(TrType::getId).collect(Collectors.toSet())); jsonGenerator.writeEndObject(); } }
TrType 实体
@Table(name = "tr_types", indexes = { @Index(name = "tr_types_type_name_uindex", columnList = "type_name", unique = true) }) @Entity @AllArgsConstructor(access = AccessLevel.PUBLIC) @NoArgsConstructor public class TrType { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long id; @Column(name = "type_name", nullable = false) private String typeName; }
已通过@JsonManagedReference和@JsonBackReference避免循环引用,trTypes字段使用FetchType.EAGER可正常获取数据,但不想给versions添加该属性。调用序列化器的MachineGroupService方法已标注@Transactional,怀疑Hibernate Session在序列化阶段已关闭。
异常原因
- Session生命周期不匹配:Spring的
@Transactional注解会在Service方法执行完毕后自动提交事务并关闭Hibernate Session。而序列化操作是在Service返回实体后才执行的,此时Session已关闭,懒加载的versions集合无法触发数据库查询完成初始化。 - 序列化器的
@Transactional无效:MachineGroupSerializer不是Spring管理的Bean,Spring无法为其创建事务代理,因此序列化方法上的@Transactional注解不会生效,无法重新打开Session。 trTypes正常的核心原因:该字段配置了FetchType.EAGER,会在查询MachineGroup实体时直接从数据库加载关联数据,无需依赖后续的Session。
解决方案
方案1:在Service层提前初始化懒加载集合
在Service的事务方法内,主动触发versions集合的加载,确保数据在Session关闭前完成初始化。可以使用Hibernate.initialize()方法或者调用集合的size()方法触发加载:
@Service public class MachineGroupService { @Autowired private MachineGroupRepository machineGroupRepository; @Transactional public MachineGroup getMachineGroupById(long id) { MachineGroup machineGroup = machineGroupRepository.findById(id) .orElseThrow(() -> new RuntimeException("MachineGroup not found")); // 主动初始化versions集合 Hibernate.initialize(machineGroup.getVersions()); // 或者调用size()方法触发加载 // machineGroup.getVersions().size(); return machineGroup; } }
方案2:使用DTO传输数据
创建与MachineGroup对应的DTO类,在Service层的事务内将实体数据转换为DTO(包括提取versions的ID集合),序列化时直接操作DTO,完全规避懒加载问题:
定义MachineGroupDTO
@Getter @Setter public class MachineGroupDTO { private long id; private Set<Long> versionIds; private Set<Long> trTypeIds; public MachineGroupDTO(MachineGroup machineGroup) { this.id = machineGroup.getId(); this.versionIds = machineGroup.getVersions().stream() .map(Version::getId) .collect(Collectors.toSet()); this.trTypeIds = machineGroup.getTrTypes().stream() .map(TrType::getId) .collect(Collectors.toSet()); } }
Service层返回DTO
@Service public class MachineGroupService { @Autowired private MachineGroupRepository machineGroupRepository; @Transactional public MachineGroupDTO getMachineGroupDTOById(long id) { MachineGroup machineGroup = machineGroupRepository.findById(id) .orElseThrow(() -> new RuntimeException("MachineGroup not found")); return new MachineGroupDTO(machineGroup); } }
此时序列化器可以直接针对DTO编写,或者使用默认序列化即可,无需处理懒加载问题。
方案3:配置Open Session In View(OSIV)
通过配置OSIV过滤器,让Hibernate Session在整个HTTP请求周期内保持打开,直到响应序列化完成。这种方式无需修改业务代码,但要注意长期打开Session可能带来的性能问题和连接占用风险。
在Spring Boot中,只需添加以下配置即可启用OSIV:
spring.jpa.open-in-view=true
方案4:批量抓取优化懒加载
如果不想使用EAGER加载,也可以通过@BatchSize注解配置批量抓取,减少N+1查询问题,同时保持懒加载特性。需要确保在Session存在时访问集合:
@ManyToMany(cascade = CascadeType.MERGE) @JoinTable(name = "machine_groups_to_versions", joinColumns = @JoinColumn(name = "machine_group_id"), inverseJoinColumns = @JoinColumn(name = "version_id")) @JsonManagedReference(value="machineGroups-versions") @BatchSize(size = 10) // 批量加载10个集合 private Set<Version> versions = new HashSet<>();
内容的提问来源于stack exchange,提问作者Gilad Dahan

