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

Hibernate Envers:如何将自定义RevisionEntity字段映射到审计查询

问题描述

我已在方案中分离审计支持与版本管理功能,目前运行正常,但不清楚如何编写查询获取所有版本,并将REVINFO中的审计数据映射到查询结果的MyEntity中。具体需求:

  • 通过REV关联MYENTITY_AUD与REVINFO表,获取lastModified相关字段;
  • created相关字段需通过REVTYPE = 0(新增操作)的子查询获取。

现有查询代码:

private List<MyEntity> allRevisions() {
  AuditReaderFactory.get(entityManager).createQuery()
    .forRevisionsOfEntity(MyEntity.class, true, true)        
    .addOrder(AuditEntity.revisionNumber().asc())
    .getResultList();
}

数据表结构:

MYENTITY table
+---------------------------------------------------------------------------+
| ID | MYDATA | CREATEDBY | CREATEDDATE | LASTMODIFIEDBY | LASTMODIFIEDDATE |
+---------------------------------------------------------------------------+

MYENTITY_AUD table
+-----------------------------+
| ID | REV | REVTYPE | MYDATA |
+-----------------------------+

REVINFO table
+-----------------------------+
| REV | REVTSTMP | MODIFIEDBY |
+-----------------------------+

相关实体代码:

@Audited
@Entity
public class MyEntity {
    
    private String myData;

    @CreatedBy
    @NotAudited
    private String createBy;

    @CreateDate
    @NotAudited
    private LocalDateTime createdDate;

    @LastModifiedBy
    @NotAudited
    private String lastModifiedBy;

    @LastModifiedDate
    @NotAudited
    private LocalDateTime lastModifiedDate;
}
@Entity
@Table(name = "REVINFO")
@RevisionEntity(MyRevisionListener.class)
public class MyRevisionEntity implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @RevisionNumber
    @Column(name = "REV")
    private int id;

    @RevisionTimestamp
    @Column(name = "REVTSTMP")
    private long timestamp;

    @Column(name = "MODIFIED_BY")
    private String modifiedBy;
    // getter/setter 省略
}
public class MyRevisionListener implements RevisionListener {

    @Override
    public void newRevision(Object revisionEntity) {
        MyRevisionEntity revision = (MyRevisionEntity) revisionEntity;
        revision.setModifiedBy(MyContext.getUserName());
    }
}
解决方案

1. 理解Envers查询返回结构

forRevisionsOfEntity默认返回三元组数组:[实体实例, 修订实体, 修订类型],而非直接返回MyEntity对象。需先接收该三元组,再手动映射审计数据。

2. 获取所有版本并映射审计数据

修改查询逻辑,处理每个版本的created和lastModified字段:

private List<MyEntity> getAllRevisionsWithAuditData(Long entityId) {
    AuditReader auditReader = AuditReaderFactory.get(entityManager);
    
    // 查询指定实体的所有修订,返回三元组:[MyEntity, MyRevisionEntity, RevisionType]
    List<Object[]> revisionResults = auditReader.createQuery()
            .forRevisionsOfEntity(MyEntity.class, false, true)
            .add(AuditEntity.id().eq(entityId)) // 若查询所有实体则移除该条件
            .addOrder(AuditEntity.revisionNumber().asc())
            .getResultList();

    List<MyEntity> result = new ArrayList<>();
    for (Object[] revisionTuple : revisionResults) {
        MyEntity entity = (MyEntity) revisionTuple[0];
        MyRevisionEntity currentRevision = (MyRevisionEntity) revisionTuple[1];
        RevisionType revType = (RevisionType) revisionTuple[2];

        // 映射lastModified字段:直接使用当前修订的审计数据
        entity.setLastModifiedBy(currentRevision.getModifiedBy());
        entity.setLastModifiedDate(LocalDateTime.ofInstant(
                Instant.ofEpochMilli(currentRevision.getTimestamp()),
                ZoneId.systemDefault()
        ));

        // 映射created字段:找到该实体的首次修订(REVTYPE=0)
        if (revType == RevisionType.ADD) {
            // 首次修订即为创建记录,直接赋值
            entity.setCreateBy(currentRevision.getModifiedBy());
            entity.setCreatedDate(LocalDateTime.ofInstant(
                    Instant.ofEpochMilli(currentRevision.getTimestamp()),
                    ZoneId.systemDefault()
            ));
        } else {
            // 非首次修订,查询该实体的首次创建记录
            MyRevisionEntity createRevision = (MyRevisionEntity) auditReader.createQuery()
                    .forRevisionsOfEntity(MyEntity.class, false, true)
                    .add(AuditEntity.id().eq(entityId))
                    .add(AuditEntity.revisionType().eq(RevisionType.ADD))
                    .setMaxResults(1)
                    .getSingleResult()[1];
            
            entity.setCreateBy(createRevision.getModifiedBy());
            entity.setCreatedDate(LocalDateTime.ofInstant(
                    Instant.ofEpochMilli(createRevision.getTimestamp()),
                    ZoneId.systemDefault()
            ));
        }

        result.add(entity);
    }
    return result;
}

3. 优化性能(批量查询场景)

若需查询多个实体的所有版本,重复查询首次修订会引发N+1问题,可先批量缓存所有实体的创建修订:

// 先缓存所有实体的创建修订记录
Map<Long, MyRevisionEntity> createRevisionCache = new HashMap<>();
List<Object[]> createRevisions = auditReader.createQuery()
        .forRevisionsOfEntity(MyEntity.class, false, true)
        .add(AuditEntity.revisionType().eq(RevisionType.ADD))
        .getResultList();
for (Object[] tuple : createRevisions) {
    MyEntity entity = (MyEntity) tuple[0];
    MyRevisionEntity revision = (MyRevisionEntity) tuple[1];
    createRevisionCache.put(entity.getId(), revision);
}

// 处理所有修订时直接从缓存取创建信息
for (Object[] revisionTuple : revisionResults) {
    MyEntity entity = (MyEntity) revisionTuple[0];
    MyRevisionEntity currentRevision = (MyRevisionEntity) revisionTuple[1];
    
    // 映射lastModified字段...
    
    MyRevisionEntity createRevision = createRevisionCache.get(entity.getId());
    entity.setCreateBy(createRevision.getModifiedBy());
    entity.setCreatedDate(LocalDateTime.ofInstant(
            Instant.ofEpochMilli(createRevision.getTimestamp()),
            ZoneId.systemDefault()
    ));
}

关键说明

  • forRevisionsOfEntity第二个参数selectEntitiesOnly设为false时,才会返回包含修订实体和类型的三元组;
  • RevisionType.ADD对应数据库中REVTYPE=0,是实体的首次创建记录;
  • 由于createdBy和createdDate标记为@NotAudited,Envers不会在审计表中保存这些字段,只能通过首次修订的REVINFO数据填充。

内容的提问来源于stack exchange,提问作者fuqsha

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 10:13:17