最近项目中有个需求,对于重要信息的变更需要记录变更历史,即变更时间、修改人、修改前的值、修改后的值,为潜在的历史追溯提供数据支持。
创建变更记录表
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的时候,在这个表中新增一条相应的变更历史。
但是细想后这里面存在一些问题:
- 各个服务在更新业务数据时要增加变更历史信息添加的逻辑
- 一般情况下前端将修改后的全量数据发送过来,后端直接进行信息的覆盖,此时后端需要对比新旧数据,将真正修改的信息记录到历史更新表中
- 对于具体哪些信息需要记录历史变更,需求可能会变
解决上述问题的方案就是将历史变更做成一个公共服务,其他业务在执行更新时调用这个服务即可,无需关注底层。对于哪些信息需要记录历史,做成可配置。
创建注解@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层用注解控制与配置需要记录变更历史的字段。