You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让Hibernate Envers使用PersistentBag而非代理集合优化性能?

解决Hibernate Envers查询修订版时集合代理触发意外加载的性能问题

问题背景

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 23:10:22