数据变更历史记录

最近项目中有个需求,对于重要信息的变更需要记录变更历史,即变更时间、修改人、修改前的值、修改后的值,为潜在的历史追溯提供数据支持。

创建变更记录表

1
2
3
4
5
6
7
8
9
10
11
12
13
create table t_update_history
(
id bigint not null auto_increment comment '标识ID',
table_name varchar(100) not null comment '表名',
record_id varchar(100) not null comment '变更的记录ID',
field_name varchar(100) not null comment '变更字段名,即表的字段名',
before_value varchar(1000) comment '修改前字段值',
after_value varchar(1000) comment '修改后字段值',
change_user_id varchar(100) not null comment '修改人ID',
change_user_name varchar(300) not null comment '修改人姓名',
change_time datetime not null comment '修改时间',
primary key (id)
);

在DAO层执行update的时候,在这个表中新增一条相应的变更历史。

但是细想后这里面存在一些问题:

  1. 各个服务在更新业务数据时要增加变更历史信息添加的逻辑
  2. 一般情况下前端将修改后的全量数据发送过来,后端直接进行信息的覆盖,此时后端需要对比新旧数据,将真正修改的信息记录到历史更新表中
  3. 对于具体哪些信息需要记录历史变更,需求可能会变

解决上述问题的方案就是将历史变更做成一个公共服务,其他业务在执行更新时调用这个服务即可,无需关注底层。对于哪些信息需要记录历史,做成可配置。

创建注解@History用于标注PO层类,代表该类对应实体需要记录变更历史;创建注解 @HistoryRecord用于标注PO层类的成员变量,代表该字段的值变更需要激励变更历史。利用注解与Java的反射机制达到可配置效果。

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface History {
String table(); // 定义实体对应的表名
}
1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface HistoryRecord {
String field(); // 定义字段对应的表字段名
}

提供更新代理UpdateAgent,代理所有更新方法。业务系统直接调用UpdateAgent.update方法进行数据的更新,而不直接调用DAO层的update方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Component
public class UpdateAgent {
@Autowired
private TUpdateHistoryService historyService; // 历史变更表service层服务

@Transactional
public void update(BaseDao dao, Object entity, String entityId) {
compareAndRecordHistory(dao, entity, entityId);
dao.update(entity);
}

private void compareAndRecordHistory(BaseDao dao, Object entity, String entityId) {
// 如果没有@History注解,不做改实体的变更历史记录
if (!entity.getClass().isAnnotationPresent(History.class)) {
return;
}

String tableName = entity.getClass().getAnnotation(History.class).table();
Object oldEntity = dao.queryObject(entityId);
if (oldEntity == entity || entity.equals(oldEntity)) {
return;
}

List<TUpdateHistoryEntity> updateEntities = Lists.newArrayList();
Field[] fields = entity.getClass().getDeclaredFields();
for (Field field: fields) {
// 如果没有@HistoryRecord 注解,不做改字段的变更历史记录
if (!field.isAnnotationPresent(HistoryRecord.class)) {
continue;
}

field.setAccessible(true);
String fieldName = field.getAnnotation(HistoryRecord.class).field();

try {
String oldField = "";
String newField = "";
if (field.get(oldEntity) != null) {
oldField = field.get(oldEntity).toString();
}
if (field.get(entity) != null) {
newField = field.get(entity).toString();
}

if (!oldField.equals(newField)) {
TUpdateHistoryEntity history = new TUpdateHistoryEntity();
history.setTableName(tableName);
history.setFieldName(fieldName);
history.setRecordId(entityId);
history.setBeforeValue(oldField);
history.setAfterValue(newField);
history.setChangeTime(new Date());
history.setChangeUserId("1");
history.setChangeUserName("admin");
updateEntities.add(history);
}
} catch (IllegalAccessException e) {
// todo
}
}

record(updateEntities);
}

private void record(List<TUpdateHistoryEntity> updateEntities) {
if (updateEntities != null) {
for (TUpdateHistoryEntity updateEntity: updateEntities) {
historyService.save(updateEntity);
}
}
}
}

业务系统调用代理类的update方法进行数据更新,需要传入DAO对象,新实体对象,实体ID。在PO层用注解控制与配置需要记录变更历史的字段。