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
相关产品推荐
相关产品推荐

