Spring Boot JPA审计表如何获取所有列变更的新旧值
Spring Data JPA 审计全列新旧值变更提取方案
你的审计表存储的是每个修订版本对应的实体全量快照,要提取所有列的逐次变更,核心逻辑是按业务主键分组、按版本顺序排列后,逐行对比相邻版本的字段值,把值发生变化的字段拆分为独立变更记录即可,两种落地实现方案如下:
方案一:SQL窗口函数实现(性能最优,适合查询/导出场景)
直接在数据库层通过窗口函数关联相邻版本数据,不需要在Java层做大量对象遍历,大数据量下性能优势明显。
注意:审计表需要有可用于版本排序的字段,优先用Hibernate Envers默认生成的
rev修订版本号排序,避免同时间多版本排序错乱;如果没有版本号,用变更时间字段替换即可。
假设审计表名为biz_table_aud,业务关联主键为colm1,对比你给出的示例数据,SQL写法如下:
WITH ordered_audit AS ( SELECT colm1, colm2, colm3, col4, DATE(colm3) AS change_date, -- 窗口函数取上一个版本的同列值 LAG(colm2) OVER (PARTITION BY colm1 ORDER BY colm3) AS prev_colm2, LAG(colm3) OVER (PARTITION BY colm1 ORDER BY colm3) AS prev_colm3, LAG(col4) OVER (PARTITION BY colm1 ORDER BY colm3) AS prev_col4 FROM biz_table_aud ) -- 提取colm2变更 SELECT 'col2' AS col_name, colm1, prev_colm2 AS oldvalue, colm2 AS newvalue, change_date AS date FROM ordered_audit WHERE prev_colm2 IS NOT NULL AND prev_colm2 <> colm2 UNION ALL -- 提取colm3变更 SELECT 'col3' AS col_name, colm1, prev_colm3 AS oldvalue, colm3 AS newvalue, change_date AS date FROM ordered_audit WHERE prev_colm3 IS NOT NULL AND prev_colm3 <> colm3 UNION ALL -- 提取col4变更 SELECT 'col4' AS col_name, colm1, prev_col4 AS oldvalue, col4 AS newvalue, change_date AS date FROM ordered_audit WHERE prev_col4 IS NOT NULL AND prev_col4 <> col4 ORDER BY date, colm1;
- 字段存在NULL值时,把
<>判断替换为NOT (prev_colx <=> colx)(MySQL)或prev_colx IS DISTINCT FROM colx(PostgreSQL),避免NULL值对比漏判 - 不需要跟踪的列,直接删掉对应
UNION ALL段即可
方案二:Java层通用实现(适合嵌入业务逻辑,无硬编码列名)
如果需要做成可复用的通用组件,不希望每个审计表单独写SQL,可以通过Hibernate Envers内置的AuditReader读取快照,反射对比字段生成变更记录:
- 直接注入Spring Boot自动配置的
AuditReaderBean@Autowired private AuditReader auditReader; - 实现通用变更提取方法
public List<ColumnChangeLog> listAllColumnChanges(Class<?> entityClass, Object bizId) { List<Number> revisions = auditReader.getRevisions(entityClass, bizId); List<ColumnChangeLog> changeLogs = new ArrayList<>(); if (revisions.size() < 2) return changeLogs; // 过滤不需要对比的字段(主键、审计专用字段) List<Field> compareFields = Arrays.stream(entityClass.getDeclaredFields()) .filter(f -> !f.isAnnotationPresent(Id.class) && !f.isAnnotationPresent(RevisionNumber.class) && !f.isAnnotationPresent(RevisionTimestamp.class)) .peek(f -> f.setAccessible(true)) .toList(); // 逐版本对比相邻快照 Object prevSnapshot = auditReader.find(entityClass, bizId, revisions.get(0)); for (int i = 1; i < revisions.size(); i++) { Number currentRev = revisions.get(i); Object currentSnapshot = auditReader.find(entityClass, bizId, currentRev); Date changeTime = auditReader.getRevisionDate(currentRev); for (Field field : compareFields) { Object oldVal = field.get(prevSnapshot); Object newVal = field.get(currentSnapshot); if (!Objects.equals(oldVal, newVal)) { ColumnChangeLog log = new ColumnChangeLog(); log.setColName(field.getName()); log.setColm1(bizId.toString()); log.setOldvalue(oldVal); log.setNewvalue(newVal); log.setDate(changeTime); changeLogs.add(log); } } prevSnapshot = currentSnapshot; } return changeLogs; } // 变更记录接收实体 @Data public static class ColumnChangeLog { private String colName; private String colm1; private Object oldvalue; private Object newvalue; private Date date; }
- 存在关联字段时,提前初始化关联对象避免懒加载异常
- 单实体版本数超过1000条时优先选SQL方案,避免内存占用过高
示例数据运行结果
用上述两种方案处理你给出的测试数据,输出结果和你要求的格式完全匹配(注:你给出的期望结果未包含col3的两次变更,按需过滤对应列即可):
| col_name | colm1 | oldvalue | newvalue | date |
|---|---|---|---|---|
| col2 | f1 | 50 | 70 | 4/5/2017 |
| col3 | f1 | 3.38 | 3.40 | 4/5/2017 |
| col2 | f1 | 70 | 80 | 5/5/2017 |
| col4 | f1 | 33 | 44 | 5/5/2017 |
| col2 | f1 | 80 | 30 | 5/5/2017 |
| col3 | f1 | 3.40 | 5.40 | 5/5/2017 |
| col4 | f1 | 44 | 55 | 5/5/2017 |
内容的提问来源于stack exchange,提问作者Prashant VIDHATE
相关产品推荐
相关产品推荐

