数据库里每张表都有 create_time、update_time,你敢保证线上每一条记录的这两个字段都是对的值吗?大部分项目靠 insert 时手写几个字段,update 时漏一个就静默吃灰。qkl-boot 里我把这层用 MyBatis-Plus 的字段自动填充 + 逻辑删除收编了,审计字段不再靠自觉。
表结构先统一
脚手架的公共字段是固定的:创建时间、更新时间、创建人、更新人、删除标记、版本号。建表的时候统一带上,别一张表一个样:
CREATETABLE`jx_order`(`id`bigintNOTNULLAUTO_INCREMENTCOMMENT'主键',`order_no`varchar(32)NOTNULLCOMMENT'订单号',`worker_id`bigintDEFAULTNULLCOMMENT'接单工人ID',`create_time`datetimeDEFAULTNULLCOMMENT'创建时间',`update_time`datetimeDEFAULTNULLCOMMENT'更新时间',`create_by`varchar(32)DEFAULTNULLCOMMENT'创建人',`update_by`varchar(32)DEFAULTNULLCOMMENT'更新人',`deleted`tinyintNOTNULLDEFAULT'0'COMMENT'逻辑删除:0未删 1已删',`version`intNOTNULLDEFAULT'0'COMMENT'乐观锁版本',PRIMARYKEY(`id`),KEY`idx_order_no`(`order_no`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='零工订单表';deleted和version一个管逻辑删除,一个给乐观锁用,都是脚手架全局统一的命名,谁也别想自己起个别名。
自动填充:一个处理器管全部表
以前业务代码里写order.setCreateTime(new Date())的地方全删掉。MyBatis-Plus 的MetaObjectHandler拦截 insert/update,统一往里塞值:
@ComponentpublicclassMyMetaObjectHandlerimplementsMetaObjectHandler{@OverridepublicvoidinsertFill(MetaObjectmetaObject){this.strictInsertFill(metaObject,"createTime",LocalDateTime.class,LocalDateTime.now());this.strictInsertFill(metaObject,"updateTime",LocalDateTime.class,LocalDateTime.now());this.strictInsertFill(metaObject,"deleted",Integer.class,0);// createBy 从当前登录上下文取,没有登录态就填 systemLonguserId=SecurityUtils.getUserId();this.strictInsertFill(metaObject,"createBy",String.class,userId!=null?userId.toString():"system");this.strictInsertFill(metaObject,"updateBy",String.class,userId!=null?userId.toString():"system");}@OverridepublicvoidupdateFill(MetaObjectmetaObject){this.strictUpdateFill(metaObject,"updateTime",LocalDateTime.class,LocalDateTime.now());LonguserId=SecurityUtils.getUserId();this.strictUpdateFill(metaObject,"updateBy",String.class,userId!=null?userId.toString():"system");}}实体上标注解,告诉 MP 哪些字段走自动填充:
@Data@TableName("jx_order")publicclassOrder{@TableId(type=IdType.ASSIGN_ID)privateLongid;privateStringorderNo;privateLongworkerId;@TableField(fill=FieldFill.INSERT)privateLocalDateTimecreateTime;@TableField(fill=FieldFill.INSERT_UPDATE)privateLocalDateTimeupdateTime;@TableField(fill=FieldFill.INSERT)privateStringcreateBy;@TableField(fill=FieldFill.INSERT_UPDATE)privateStringupdateBy;@TableLogic@TableField(fill=FieldFill.INSERT)privateIntegerdeleted;@VersionprivateIntegerversion;}这么一套下来,业务代码里 insert/update 就只写业务字段,审计字段全是系统自动补的。
踩坑实录:批量更新把 create_time 覆盖成了当前时间
现象:运营后台有个"批量修改订单备注"的功能,改完一看,一批订单的 create_time 全变成了刚才的时间。用户投诉"订单创建时间怎么是昨天下午"。
排查过程:先看 SQL,MP 生成的 update 语句里带了create_time = ?,说明 create_time 进入了更新字段集合。再看实体,createTime上标的是FieldFill.INSERT,理论上 update 不该带上它——但批量更新走的不是单条 update。
定位思路:翻代码发现批量改备注用的是updateBatchById,而实体里 create_time 标了@TableField(fill = FieldFill.INSERT),按 MP 语义 update 时不该填充——问题出在实体上还有个隐藏点:createTime 同时被insertFill和updateFill都写了值。排查到MyMetaObjectHandler.updateFill里,为了省事把 createBy/updateBy 一起塞的时候,顺手写了strictUpdateFill(metaObject, "createTime", ...)——多了这一行,update 时 createTime 也被填充了。
最终解决:把 updateFill 里的 createTime 那行删掉,只保留 updateTime 和 updateBy。回归测试:单条 update、updateBatchById、wrapper.update 三种路径都验一遍,确认 create_time 不再出现在 update set 里。
逻辑删除:deleted 字段上的 @TableLogic
物理删除一时爽,出了事想恢复只能找备份。qkl-boot 全局用逻辑删除,MP 的@TableLogic会自动把 delete 变成 update,查询自动带deleted=0:
// 业务代码照常写orderService.removeById(orderId);// 实际执行的 SQL:// UPDATE jx_order SET deleted=1, update_time=? WHERE id=? AND deleted=0查询侧不用管,MP 生成的 select 都自动追加AND deleted=0。但注意唯一索引的坑:比如订单号 order_no 建了唯一索引,逻辑删除后同一订单号再发一单,插入会撞唯一索引。解法两个:把唯一索引改成(order_no, deleted)联合唯一,或者删除时把 order_no 加后缀改写(比如order_no + '#del' + id)。零工场景我选的联合唯一索引,简单直接,数据也不变形。
可以直接抄的清单
- 公共字段统一:
create_time/update_time/create_by/update_by/deleted/version,建表 DDL 直接复制 MetaObjectHandler只负责 insert/update 填充,updateFill 里千万别写 createTime- 实体注解三件套:
@TableField(fill=...)、@TableLogic、@Version,缺一个功能就歪 - 逻辑删除 + 唯一索引 = 联合唯一索引
(业务唯一键, deleted),别单独建 - 批量更新(updateBatchById)路径也要回归验证,它和单条 update 的填充行为不完全一样
项目源码:https://gitee.com/gzqkl/qkl-boot