如何让Hibernate Envers使用PersistentBag而非代理集合优化性能?
问题背景
基于JPA+Hibernate+Envers的Java项目中,原生Hibernate查询关联实体时,集合会被包装为PersistentBag/PersistentSet(实现PersistentCollection接口),支持懒加载——仅当访问集合元素时才触发数据库查询。但通过AuditReader查询修订版时,集合会被包装为ListProxy/SetProxy,导致仅访问父对象就会加载整个集合;结合Spring Hateoas生成_embedded响应时,会触发大量不必要的数据库查询,性能急剧下降。
已尝试的无效方案:
- 忽略修订版中的嵌入资源:性能提升但丢失关联引用
- 配置集合注解的
fetch属性:Envers未生效 - 使用投影:端点高度通用无法适配
可行解决方案
1. 自定义Envers集合映射器,替换为PersistentBag/PersistentSet
Envers通过CollectionMapper接口处理集合的序列化与反序列化,默认实现会生成代理集合。我们可以重写该接口,将集合包装为Hibernate原生的PersistentBag/PersistentSet,保持懒加载行为。
步骤1:实现自定义CollectionMapper
import org.hibernate.envers.internal.entities.mapper.DefaultCollectionMapper; import org.hibernate.envers.internal.entities.mapper.CollectionEntry; import org.hibernate.collection.spi.PersistentBag; import org.hibernate.collection.spi.PersistentSet; import org.hibernate.engine.spi.SessionImplementor; import java.util.List; import java.util.Set; public class LazyCollectionMapper extends DefaultCollectionMapper { @Override public Object mapCollection(Object originalCollection, CollectionEntry entry, SessionImplementor session) { if (originalCollection instanceof List) { PersistentBag persistentBag = new PersistentBag(session); persistentBag.addAll((List<?>) originalCollection); // 标记为未初始化,模拟原生懒加载集合 persistentBag.setInitialized(false); persistentBag.setOwner(entry.getOwner()); persistentBag.setRole(entry.getRole()); return persistentBag; } else if (originalCollection instanceof Set) { PersistentSet persistentSet = new PersistentSet(session); persistentSet.addAll((Set<?>) originalCollection); persistentSet.setInitialized(false); persistentSet.setOwner(entry.getOwner()); persistentSet.setRole(entry.getRole()); return persistentSet; } return super.mapCollection(originalCollection, entry, session); } }
步骤2:注册自定义映射器
在Hibernate配置中指定Envers使用自定义集合映射器:
- Spring Boot项目:在
application.properties中添加spring.jpa.properties.hibernate.envers.collection_mapper=com.yourpackage.LazyCollectionMapper - 传统JPA项目:在
persistence.xml的<properties>中添加<property name="hibernate.envers.collection_mapper" value="com.yourpackage.LazyCollectionMapper"/>
2. 拦截Spring Hateoas序列化过程,避免集合初始化
如果无法修改Envers的集合代理,可以通过Jackson序列化配置,阻止Spring Hateoas生成_embedded时触发集合加载。
方案2.1:自定义Jackson序列化器
为Envers的代理集合编写序列化器,仅输出集合元数据(如大小)而非加载元素:
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.hibernate.envers.internal.collections.ListProxy; import org.hibernate.envers.internal.collections.SetProxy; import java.io.IOException; public class EnversLazyCollectionSerializer extends JsonSerializer<Object> { @Override public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value instanceof ListProxy) { gen.writeStartObject(); gen.writeNumberField("size", ((ListProxy<?>) value).size()); gen.writeBooleanField("lazyLoaded", true); gen.writeEndObject(); } else if (value instanceof SetProxy) { gen.writeStartObject(); gen.writeNumberField("size", ((SetProxy<?>) value).size()); gen.writeBooleanField("lazyLoaded", true); gen.writeEndObject(); } else { serializers.defaultSerializeValue(value, gen); } } }
然后在修订版实体的集合字段上添加注解:
@JsonSerialize(using = EnversLazyCollectionSerializer.class) private List<ChildEntity> children;
方案2.2:修改Spring Hateoas的RepresentationModel组装逻辑
自定义RepresentationModelAssembler,在构建响应时跳过集合的初始化,仅添加集合的关联链接而非嵌入数据:
import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.server.RepresentationModelAssembler; public class RevisionEntityAssembler implements RepresentationModelAssembler<YourRevisionEntity, EntityModel<YourRevisionEntity>> { @Override public EntityModel<YourRevisionEntity> toModel(YourRevisionEntity entity) { EntityModel<YourRevisionEntity> model = EntityModel.of(entity); // 仅添加集合的关联链接,不嵌入集合元素 model.add(linkTo(methodOn(ChildController.class).getChildren(entity.getId())).withRel("children")); return model; } }
3. 手动控制Envers查询的集合加载策略
通过AuditQuery手动构建查询,强制指定集合的懒加载模式:
import org.hibernate.envers.AuditReader; import org.hibernate.envers.query.AuditEntity; import org.hibernate.FetchMode; // 获取AuditReader实例 AuditReader auditReader = AuditReaderFactory.get(entityManager); // 构建修订版查询,指定集合懒加载 YourRevisionEntity revision = (YourRevisionEntity) auditReader.createQuery() .forRevisionsOfEntity(YourEntity.class, false, true) .add(AuditEntity.id().eq(entityId)) .add(AuditEntity.revisionNumber().eq(revisionId)) .setFetchMode("children", FetchMode.LAZY) .getSingleResult();
总结
优先推荐方案1,通过自定义Envers集合映射器替换为Hibernate原生懒加载集合,完全保持与原生查询一致的行为,无需修改业务代码或序列化逻辑。若方案1无法适配,可选择方案2拦截序列化过程,或方案3手动控制查询策略。
内容的提问来源于stack exchange,提问作者Dignity

