想象这样一个场景:你优雅地配置了 MyBatis-Plus
的 MetaObjectHandler
,创建、更新操作的时间戳、操作人信息都能自动填充,开发效率飞升。但很快挑战接踵而至——修改数据时 update_date
没更新?removeByIds
删除后数据库却没有更新人记录?甚至一次常规更新操作后,后台数据竟然“忘了”记录这次修改!这些困扰很多开发者的 updateFill
“失灵”、更新及删除操作的填充缺失问题,往往源自对 MP
内部机制理解不够深入或特定方法的误用。本文将深挖这些问题根源,提供可落地的解决方案,并确保你在更新操作乃至逻辑删除时,也能如丝般顺滑地实现字段自动维护。掌握这些技巧,让你的数据层不仅健壮,而且智能高效!
疑难解析:updateFill(strictUpdateFill) 失效问题 问题分析 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 @TableField(fill = FieldFill.INSERT) private Date createDate;@TableField(fill = FieldFill.INSERT) private String createBy;@TableField(fill = FieldFill.INSERT_UPDATE) private Date updateDate;@TableField(fill = FieldFill.INSERT_UPDATE) private String updateBy;@TableLogic @TableField(fill = FieldFill.INSERT) private Integer delFlag;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill (MetaObject metaObject) { String currentUserId = UserUtils.getCurrentUserId(); Date currenteDate = new Date (); this .strictInsertFill(metaObject, "delFlag" , Integer.class, 0 ); this .strictInsertFill(metaObject, "createBy" , String.class, currentUserId); this .strictInsertFill(metaObject, "createDate" , Date.class, currenteDate); this .strictInsertFill(metaObject, "updateBy" , String.class, currentUserId); this .strictInsertFill(metaObject, "updateDate" , Date.class, currenteDate); } @Override public void updateFill (MetaObject metaObject) { this .strictUpdateFill(metaObject, "updateBy" , String.class, UserUtils.getCurrentUserId()); this .strictUpdateFill(metaObject, "updateDate" , Date.class, new Date ()); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 public void test () { User user = new User (); user.setId("1" ); user.setName("测试" ); save(user); User updateUser = new User (); updateUser.setId("1" ); updateUser.setName("测试" ); updateById(updateUser); }
出现的问题: 在插入数据时,逻辑删除标记和新增、修改时间及操作人都被赋值了;但是在更新数据时,修改时间和操作人没有被成功更新。
问题原因: 在官方文档 自动填充字段|Mybatis-Plus 的注意事项中,明确提到了: MetaObjectHandler
提供的默认方法的策略均为: 如果属性有值则不覆盖, 如果填充值为null则不填充
在 MetaObjectHandler
中,strictUpdateFill()
方法被设计为一种“严格模式”的更新填充。与基础的 fillStrategy()
或直接操作 setFieldValByName()
不同,它的核心行为是:
仅当目标字段严格符合 FieldFill.UPDATE 或 FieldFill.INSERT_UPDATE 策略,且该字段在 entity 对象中的值为 null 时,才会执行填充
这就是导致它看似“失效”的根本原因。你期望它更新字段(无论业务代码是否设置),而它却严格遵守了“只在字段为空时才填充”的规则。简而言之,MP的自动填充功能的前提是填充字段要求为null。
根治方案 方案一:更新前手动设置字段为 null(不推荐) 1 2 3 4 5 6 7 8 9 10 11 public void examplePlanOne () { User user = getById("1" ); user.setName("测试" ); user.setUpdateBy(null ); user.setUpdateDate(null ); updateById(user); }
侵入性强: 需要在业务逻辑代码的多个地方重复编写清空字段的代码,污染业务逻辑,与业务无关
易遗漏: 尤其是在多人协作或复杂业务流中,开发者很容易忘记这一步
潜在错误: 如果清空操作与某些业务逻辑顺序依赖处理不当,可能导致意料之外的结果
方案二:重写 strictUpdateFill 方法逻辑(不推荐) 在你的自定义 MetaObjectHandler
类中,重写 strictUpdateFill
方法本身,改变其“非空跳过”的核心行为
1 2 3 4 5 6 7 8 9 10 11 @Override public MetaObjectHandler strictFillStrategy (MetaObject metaObject, String fieldName, Supplier<?> fieldVal) { { Object obj = fieldVal.get(); if (Objects.nonNull(obj)) { metaObject.setValue(fieldName, obj); } } return this ; }
(引用于:CSDN:《mybatis-plus updateFill更新失效》 )
这个方案确实也可以实现强制填充的功能,但违背 strict
语义: strictFillStrategy
中的 strict
本身就蕴含了“严格检查条件(字段为空)才填充”的含义。且在上层源码: 中可以发现,该方案影响范围过大,strictFillStrategy
是一个 protected
方法,它被 strictInsertFill
和 strictUpdateFill
共同调用。该重写不仅会影响 updateFill
中的 strictUpdateFill
,也会影响 insertFill
中的 strictInsertFill!
后果可能会出现:原本 strictInsertFill
也只会在字段为 null
时填充(这是插入时通常期望的行为)。重写后,strictInsertFill
也会变成强制填充,可能会覆盖你在插入前设置的值(例如从 DTO
拷贝过来的值)。这通常不是插入操作期望的行为
故此,不推荐此方案!!
方案三:在 updateFill() 中使用 setFieldValByName (最常用、平衡的解决方案,推荐方案) 这是 MP
提供的强制填充方法。它不受字段在 entity
中是否为空值的限制。在你自定义的 MetaObjectHandler
的 updateFill()
方法中,放弃 strictUpdateFill()
,改用 setFieldValByName
对目标字段进行直接设置。这实际上就是绕过 strictUpdateFill
逻辑的核心推荐方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill (MetaObject metaObject) { String currentUserId = UserUtils.getCurrentUserId(); Date currenteDate = new Date (); this .strictInsertFill(metaObject, "delFlag" , Integer.class, 0 ); this .strictInsertFill(metaObject, "createBy" , String.class, currentUserId); this .strictInsertFill(metaObject, "createDate" , Date.class, currenteDate); this .strictInsertFill(metaObject, "updateBy" , String.class, currentUserId); this .strictInsertFill(metaObject, "updateDate" , Date.class, currenteDate); } @Override public void updateFill (MetaObject metaObject) { this .setFieldValByName("updateBy" , UserUtils.getCurrentUserId(), metaObject); this .setFieldValByName("updateDate" , new Date (), metaObject); } }
实现强制填充: 最直接有效地解决您的问题,不管业务代码是否给字段赋值
符合框架接口: 使用的是 MP
公开提供的 setFieldValByName
方法,没有破坏框架结构或核心类
代码集中可控: 填充逻辑依然集中在 MetaObjectHandler
中(符合职责单一),业务代码保持干净
升级兼容性好: 只要 MP
不删除 setFieldValByName
,这个方法通常非常稳定
设计权衡: 强制覆盖了业务代码显式设置的值(这就是我们需要的“失效”问题的解决方案,但本身是一种设计上的权衡)
需谨慎考虑业务含义: 在某些非常特殊的情况下,业务层可能需要显式控制该字段值(如数据迁移、特定回滚操作),强制填充可能会干扰这些场景。但记录“最后更新时间/更新人”这类元数据字段绝大部分时候都是期望强制更新的
疑难解析:LambdaUpdateWrapper 更新操作自动填充字段失效问题 问题分析 1 2 3 4 5 6 7 8 9 10 11 12 public void test () { lambdaUpdate() .eq(BaseEntity::getId, 1 ) .set(User::getName, "测试" ) .update(); LambdaUpdateWrapper<User> lqw = new LambdaUpdateWrapper <>(); lqw.eq(User::getId, 1 ).set(User::getName, "测试" ); update(lqw); }
出现的问题: 当使用 LambdaUpdateWrapper
进行更新操作时,自动填充字段也无法生效:
问题原因: 在官方文档 自动填充字段|Mybatis-Plus 的注意事项中,明确提到了:
在 update(T entity, Wrapper<T> updateWrapper)
时,entity
不能为空,否则自动填充失效。 在 update(Wrapper<T> updateWrapper)
时不会自动填充,需要手动赋值字段条件。 所以,MyBatis-Plus
的自动填充机制只对实体对象生效,当 LambdaUpdateWrapper
的 update()
方法不接收实体参数,无法触发 MetaObjectHandler
。
笔者追了一下 Debug
简单验证了官方的这个说法:
1 2 3 4 lambdaUpdate() .eq(BaseEntity::getId, "1" ) .set(User::getNo, "123" ) .update(new User ());
根治方案 方案一:手动传入实体对象(麻烦,不推荐) 1 2 3 4 5 6 7 8 9 10 11 12 public void test () { lambdaUpdate() .eq(BaseEntity::getId, 1 ) .set(User::getName, "测试" ) .update(new User ()); LambdaUpdateWrapper<User> lqw = new LambdaUpdateWrapper <>(); lqw.eq(User::getId, 1 ).set(User::getName, "测试" ); update(new User (), lqw); }
tip:这种写法之所以能够触发自动填充,是因为它巧妙地利用了 MyBatis-Plus
的填充机制核心原理
能实现自动填充的机制解析 调用正确的重载方法 .update(new User())
实际调用的是 MyBatis-Plus
的这个重载方法:这等价于:
1 2 3 4 5 6 getBaseMapper().update( new User (), lambdaUpdateWrapper );
符合官方文档注意事项中:「在 update(T entity, Wrapper<T> updateWrapper)
时,entity
不能为空,否则自动填充失效。」的描述
触发填充机制的黄金条件 当 MyBatis-Plus
执行 update(T entity, Wrapper<T> updateWrapper)
时:
实体参数非空 → 触发 MetaObjectHandler.updateFill()
Wrapper
包含 SET
条件 → 决定更新哪些字段
字段优先级规则 当实体字段和 Wrapper
的 set
字段不冲突时 → 两者都会包含在 SQL
中
当实体字段和 Wrapper
的 set 字段冲突时 → Wrapper
的 set
值优先
方案二:类型安全的增强版 UpdateWrapper 对于新项目或允许修改 Service
层的项目,我们可以创建一个类型安全的增强版 Wrapper
:
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 public class AutoFillLambdaUpdateWrapper <T> { private final IService<T> service; private final LambdaUpdateWrapper<T> wrapper; private final Class<T> entityClass; public AutoFillLambdaUpdateWrapper (IService<T> service, Class<T> entityClass) { this .service = service; this .entityClass = entityClass; this .wrapper = new LambdaUpdateWrapper <>(); } public AutoFillLambdaUpdateWrapper<T> eq (SFunction<T, ?> column, Object val) { wrapper.eq(column, val); return this ; } public AutoFillLambdaUpdateWrapper<T> ne (SFunction<T, ?> column, Object val) { wrapper.ne(column, val); return this ; } public AutoFillLambdaUpdateWrapper<T> in (SFunction<T, ?> column, Collection<?> coll) { wrapper.in(column, coll); return this ; } public AutoFillLambdaUpdateWrapper<T> set (SFunction<T, ?> column, Object val) { wrapper.set(column, val); return this ; } public AutoFillLambdaUpdateWrapper<T> setSql (String sql) { wrapper.setSql(sql); return this ; } public AutoFillLambdaUpdateWrapper<T> and (Consumer<LambdaUpdateWrapper<T>> consumer) { wrapper.and(consumer); return this ; } public AutoFillLambdaUpdateWrapper<T> or (Consumer<LambdaUpdateWrapper<T>> consumer) { wrapper.or(consumer); return this ; } public AutoFillLambdaUpdateWrapper<T> apply (String applySql, Object... values) { wrapper.apply(applySql, values); return this ; } public boolean update () { try { T entity = entityClass.getDeclaredConstructor().newInstance(); return service.update(entity, wrapper); } catch (Exception e) { throw new RuntimeException ("创建实体实例失败" , e); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class UserServiceImpl extends ServiceImpl <UserMapper, User> { public AutoFillLambdaUpdateWrapper<User> autoFillUpdate () { return new AutoFillLambdaUpdateWrapper <>(this , User.class); } public void test () { autoFillUpdate() .eq(BaseEntity::getId, 1 ) .set(User::getName, "测试" ) .update(); } }
保持链式调用风格,与MyBatis-Plus原生API高度相似
方案三:全局拦截器自动注入 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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 import com.baomidou.mybatisplus.annotation.FieldFill;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableName;import com.baomidou.mybatisplus.core.conditions.Wrapper;import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.baomidou.mybatisplus.core.metadata.TableInfo;import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;import com.baomidou.mybatisplus.core.toolkit.Constants;import lombok.Data;import lombok.experimental.Accessors;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.mapping.SqlCommandType;import org.apache.ibatis.plugin.Interceptor;import org.apache.ibatis.plugin.Intercepts;import org.apache.ibatis.plugin.Invocation;import org.apache.ibatis.plugin.Signature;import org.springframework.stereotype.Component;import java.lang.reflect.Field;import java.lang.reflect.ParameterizedType;import java.lang.reflect.Type;import java.util.Arrays;import java.util.Date;import java.util.List;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ConcurrentMap;import java.util.stream.Collectors;@Component @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) public class AutoFillUpdateInterceptor implements Interceptor { private static final String WRAPPER = "ew" ; private static final String PARAM2 = "param2" ; private final ConcurrentMap<Class<?>, EntityMeta> metaCache = new ConcurrentHashMap <>(); @Override public Object intercept (Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement) invocation.getArgs()[0 ]; if (ms.getSqlCommandType() != SqlCommandType.UPDATE) { return invocation.proceed(); } Object parameter = invocation.getArgs()[1 ]; if (parameter == null || !isUpdateCandidate(parameter)) { return invocation.proceed(); } Class<?> entityClass = resolveEntityClass(ms); if (entityClass == null ) { return invocation.proceed(); } EntityMeta meta = metaCache.computeIfAbsent(entityClass, this ::buildMeta); if (parameter instanceof Map) { @SuppressWarnings("unchecked") Map<String, Object> paramMap = (Map<String, Object>) parameter; Wrapper<?> wrapper = extractWrapper(paramMap); Object et = paramMap.getOrDefault(Constants.ENTITY, null ); if (et != null ) { meta.fillIntoEntity(et); } else if (wrapper != null ) { Object etInstance = entityClass.getDeclaredConstructor().newInstance(); meta.fillIntoEntity(etInstance); paramMap.put(Constants.ENTITY, etInstance); } } else { meta.fillIntoEntity(parameter); } return invocation.proceed(); } private boolean isUpdateCandidate (Object param) { if (param instanceof Wrapper) { return true ; } if (param instanceof Map) { Map<?, ?> m = (Map<?, ?>) param; return m.getOrDefault(Constants.ENTITY, null ) != null || m.get(WRAPPER) instanceof Wrapper || m.get(PARAM2) instanceof LambdaUpdateWrapper; } return param.getClass().isAnnotationPresent(TableName.class); } private Wrapper<?> extractWrapper(Map<String, Object> map) { Object ew = map.get(WRAPPER); if (ew instanceof Wrapper) { return (Wrapper<?>) ew; } Object p2 = map.get(PARAM2); if (p2 instanceof LambdaUpdateWrapper) { return (Wrapper<?>) p2; } return null ; } private Class<?> resolveEntityClass(MappedStatement ms) { try { String mapperId = ms.getId(); String className = mapperId.substring(0 , mapperId.lastIndexOf('.' )); Class<?> mapper = Class.forName(className); for (Type t : mapper.getGenericInterfaces()) { if (t instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) t; if (!BaseMapper.class.isAssignableFrom((Class<?>) pt.getRawType())) { continue ; } Type ent = pt.getActualTypeArguments()[0 ]; if (ent instanceof Class) { return (Class<?>) ent; } } } TableInfo ti = TableInfoHelper.getTableInfo(mapper); return null == ti ? null : ti.getEntityType(); } catch (Exception e) { return null ; } } private EntityMeta buildMeta (Class<?> entityCls) { List<BaseUpdateInfo> updates = Arrays.asList( BaseUpdateInfo.of("update_by" , "updateBy" , UserUtils.getCurrentUserId()), BaseUpdateInfo.of("update_date" , "updateDate" , new Date ()) ); List<Field> annotated = Arrays.stream(entityCls.getDeclaredFields()) .filter(f -> { TableField tf = f.getAnnotation(TableField.class); return null != tf && (FieldFill.UPDATE == tf.fill() || FieldFill.INSERT_UPDATE == tf.fill()); }) .peek(f -> f.setAccessible(true )) .collect(Collectors.toList()); return new EntityMeta (updates, annotated); } private static class EntityMeta { final List<BaseUpdateInfo> updates; final List<Field> fieldsToFill; EntityMeta(List<BaseUpdateInfo> updates, List<Field> fields) { this .updates = updates; this .fieldsToFill = fields; } void fillIntoEntity (Object et) { for (BaseUpdateInfo u : updates) { try { Field f = et.getClass().getDeclaredField(u.entityName); f.setAccessible(true ); if (f.get(et) == null ) { f.set(et, u.value); } } catch (NoSuchFieldException | IllegalAccessException ignored) { } } for (Field f : fieldsToFill) { updates.stream() .filter(u -> f.getName().equalsIgnoreCase(u.entityName)) .findFirst() .ifPresent(u -> { try { f.set(et, u.value); } catch (IllegalAccessException ignored) { } }); } } } @Data(staticConstructor = "of") @Accessors(chain = true) static class BaseUpdateInfo { private final String fieldName; private final String entityName; private final Object value; } }
目前仅测试了mybatis-plus-boot-starter:3.4.2~3.5.12版本,均支持
需要处理版本兼容性(支持3.4.2~截止目前最新3.5.12版本,其余版本微测试,后续可能不支持)
核心思路:在执行更新操作前进行拦截,动态补充缺失的实体对象或更新字段值
主要解决方案:对于纯 Wrapper
更新:创建一个空实体对象,自动填充所需字段值(如 updateBy
、updateDate
),然后将该实体对象放入参数中,让 MyBatis-Plus
能生成包含这些字段的 SET 语句
总结图 graph TD
A[LambdaUpdateWrapper更新] --> B{是否传入实体?}
B -->|是| C[触发自动填充]
B -->|否| D[填充字段缺失]
D --> E[解决方案]
E --> F[方案一:传入空实体]
E --> G[方案二:增强版 UpdateWrapper]
E --> H[方案三:全局拦截器] 疑难解析:RemoveById、RemoveByIds 等删除操作的自动填充失效问题 历程与思路 起初,笔者想当然地认为拦截器已实现填充逻辑,删除操作理应“手到擒来”,但现实很快泼了冷水。查阅官方文档删除接口自动填充功能失效怎么办 深入了解了一下才明白原因:
因为逻辑删除走的是 SqlCommandType.DELETE
在执行 removeById(id)
或者 lambdaUpdate().remove()
时,MP
拦截器会把 DELETE
操作转换为 UPDATE
,但是它仍然在内部以 SqlCommandType.DELETE
来标记,因此 MP
的自动填充(updateFill)
逻辑只对 SqlCommandType.UPDATE
生效,不会对这种“伪 UPDATE
”走自动填充流程。
在 Mybatis Plus
低版本(3.5.0 以下)中,deleteById
方法本身不支持自动填充,此外,遍历调用 deleteById
实现批量删除也并非高效之选。随后虽在文档中看到过时的方法四,但出于兼容性考虑(官方已在 3.5.0+ 废弃该方案)及批量删除的效率问题,不得不另寻他法。
随后又有了新思路:借鉴 LogicDeleteByIdWithFill
的思路,尝试定制 CustomizeDeleteById
和 CustomizeDeleteBatchIds
等以覆盖默认方法。然而,在即将完成了这个思路时,突然想起来这些实现类在 Mapper
接口初始化阶段就被注册并缓存,此时填充逻辑只能获取到 Id
,无法注入除固定时间外的动态值(如操作人 Id
),此路再度受(bai)阻(gei)。 咱中国人老话说得好:写(lai)都写(lai)了…那就把他完成了吧,代码贴一个出来(如有需要其他可以联系笔者):
重写方法(废案)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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 public class CustomizeDeleteById extends AbstractMethod { @Override public MappedStatement injectMappedStatement (Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE_BY_ID; String sql; if (tableInfo.getLogicDeleteFieldInfo() != null ) { List<TableFieldInfo> fieldInfos = tableInfo.getFieldList().stream() .filter(TableFieldInfo::isWithUpdateFill) .collect(Collectors.toList()); if (CollectionUtils.isNotEmpty(fieldInfos)) { StringBuilder sqlSet = new StringBuilder ("SET " ); for (TableFieldInfo field : fieldInfos) { String property = field.getProperty(); if ("updateDate" .equals(property) || "createDate" .equals(property)) { sqlSet.append(field.getColumn()).append("=" ).append(getCurrentTimeFunction()).append(", " ); } else if ("updateBy" .equals(property) || "createBy" .equals(property)) { String currentUser = getCurrentUserId(); sqlSet.append(field.getColumn()).append("='" ).append(escapeSqlValue(currentUser)).append("', " ); } else if ("delFlag" .equals(property)) { sqlSet.append(field.getColumn()).append("=" ) .append(tableInfo.getLogicDeleteFieldInfo().getLogicNotDeleteValue()).append(", " ); } else { sqlSet.append(field.getColumn()).append("=#{et." ).append(field.getProperty()).append("}, " ); } } if (sqlSet.length() > 4 ) { sqlSet.setLength(sqlSet.length() - 2 ); } sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlSet, tableInfo.getKeyColumn(), tableInfo.getKeyProperty(), tableInfo.getLogicDeleteSql(true , true )); } else { sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), this .sqlLogicSet(tableInfo), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(), tableInfo.getLogicDeleteSql(true , true )); } } else { sqlMethod = SqlMethod.DELETE_BY_ID; sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty()); } SqlSource sqlSource = this .languageDriver.createSqlSource(this .configuration, sql, modelClass); return this .addUpdateMappedStatement(mapperClass, modelClass, this .getMethod(sqlMethod), sqlSource); } private String getCurrentTimeFunction () { String dbType = configuration.getVariables().getProperty("dbType" , "mysql" ); switch (dbType.toLowerCase()) { case "oracle" : return "SYSDATE" ; case "sqlserver" : return "GETDATE()" ; case "postgresql" : return "CURRENT_TIMESTAMP" ; default : return "NOW()" ; } } private String getCurrentUserId () { return UserUtils.getCurrentUserId(); } private String escapeSqlValue (String value) { if (value == null ) { return "" ; } return value.replace("'" , "''" ); } @Override public String getMethod (SqlMethod sqlMethod) { return "deleteById" ; } }
自动填充的终极突破:编译前动态修改 SQL 在自定义 Mapper
方案碰壁后,笔者意识到问题的本质:MP
的删除操作在初始化阶段就已固化,运行时无法注入动态值。既然运行时处处受限,何不釜底抽薪——直接在 SQL
诞生前完成改造?(咳咳、怎么感觉有点中二了呢…)
于是就诞生了下面这个方案:在 StatementHandler.prepare
阶段拦截(此时 SQL
已构建但未编译),随后在 SET
后进行注入,使用 ParameterMapping.Builder
保持预编译安全性,防止 SQL
注入(值都是其他安全方法赋的,好像有点脱裤子放屁了…),通过判断原 SQL
中是否已经包含这些列,如果已经包含则不添加,避免重复添加,修改后通过 invocation.proceed() 继续执行原逻辑(使用这个方案,就不需要上面的(AutoFillUpdateInterceptor
)方案了)
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 73 74 75 76 77 78 79 80 81 82 83 import com.baomidou.mybatisplus.core.toolkit.PluginUtils;import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;import org.apache.ibatis.executor.statement.StatementHandler;import org.apache.ibatis.mapping.*;import org.apache.ibatis.plugin.*;import org.apache.ibatis.reflection.MetaObject;import org.apache.ibatis.reflection.SystemMetaObject;import org.apache.ibatis.session.Configuration;import org.springframework.stereotype.Component;import java.sql.Connection;import java.util.*;@Component @Intercepts({@Signature( type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} )}) public class UpdateSqlInterceptor2 extends AbstractSqlParserHandler implements Interceptor { @Override public Object intercept (Invocation invocation) throws Throwable { StatementHandler handler = PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObj = SystemMetaObject.forObject(handler); this .sqlParser(metaObj); MappedStatement ms = (MappedStatement) metaObj.getValue("delegate.mappedStatement" ); if (ms.getSqlCommandType() != SqlCommandType.UPDATE || ms.getStatementType() == StatementType.CALLABLE) { return invocation.proceed(); } BoundSql boundSql = (BoundSql) metaObj.getValue("delegate.boundSql" ); String originalSql = boundSql.getSql(); if (!originalSql.toUpperCase().contains(" SET " )) { return invocation.proceed(); } Map<String, Object> additionalParams = new HashMap <>(2 ); additionalParams.put("update_by" , UserUtils.getCurrentUserId()); additionalParams.put("update_date" , new Date ()); int setIndex = originalSql.toUpperCase().indexOf(" SET " ); StringBuilder newSql = new StringBuilder (originalSql); StringBuilder injectedSql = new StringBuilder (); List<ParameterMapping> injectedMappings = new ArrayList <>(); Configuration configuration = ms.getConfiguration(); for (Map.Entry<String, Object> entry : additionalParams.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); injectedSql.append(key).append(" = ?, " ); injectedMappings.add(new ParameterMapping .Builder(configuration, key, value.getClass()).build()); boundSql.setAdditionalParameter(key, value); } newSql.insert(setIndex + 5 , injectedSql); List<ParameterMapping> finalMappings = new ArrayList <>(injectedMappings); finalMappings.addAll(boundSql.getParameterMappings()); metaObj.setValue("delegate.boundSql.sql" , newSql.toString()); metaObj.setValue("delegate.boundSql.parameterMappings" , finalMappings); return invocation.proceed(); } @Override public Object plugin (Object target) { return target instanceof StatementHandler ? Plugin.wrap(target, this ) : target; } }
当你执行了如下 SQL
:
1 2 3 UPDATE table1 SET del_flag = 1 WHERE field = ? AND del_flag = 0 ;UPDATE table2 SET field1 = ?, field2 = ? WHERE field3 = ?;
拦截器将自动转化为:
1 2 3 UPDATE table1 SET update_by = ?, update_date = ?, del_flag = 1 WHERE field = ? AND del_flag = 0 ;UPDATE table2 SET update_by = ?, update_date = ?, field1 = ?, field2 = ? WHERE field3 = ?;
参数信息示例:
1 2 3 ==> Parameters: 1(String), 2025-07-20 23:52:18.037(Timestamp), 29(String) ==> Parameters: 1(String), 2025-07-25 17:52:18.037(Timestamp), 29(String), 30(Integer), abc(String)
当然,我们系统的数据库设计中,可能不是所有表都会有审计字段(修改时间、修改人),所以上述代码还可以进行一个扩展:
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 73 74 75 76 77 78 @Component @Intercepts({@Signature( type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} )}) public class UpdateSqlInterceptor2 extends AbstractSqlParserHandler implements Interceptor { private static final Set<String> NO_AUDIT_TABLES = new HashSet <>(Arrays.asList("test_table_one" , "test_table_two" )); @Override public Object intercept (Invocation invocation) throws Throwable { StatementHandler handler = PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObj = SystemMetaObject.forObject(handler); this .sqlParser(metaObj); MappedStatement ms = (MappedStatement) metaObj.getValue("delegate.mappedStatement" ); if (ms.getSqlCommandType() != SqlCommandType.UPDATE || ms.getStatementType() == StatementType.CALLABLE) { return invocation.proceed(); } BoundSql boundSql = (BoundSql) metaObj.getValue("delegate.boundSql" ); String originalSql = boundSql.getSql(); String table = originalSql.substring(originalSql.indexOf("update " ) + 7 , originalSql.indexOf(" " , originalSql.indexOf("update " ) + 7 )); if (NO_AUDIT_TABLES.contains(table)) { return invocation.proceed(); } if (!originalSql.toUpperCase().contains(" SET " )) { return invocation.proceed(); } Map<String, Object> additionalParams = new HashMap <>(2 ); additionalParams.put("update_by" , UserUtils.getCurrentUserId()); additionalParams.put("update_date" , new Date ()); int setIndex = originalSql.toUpperCase().indexOf(" SET " ); StringBuilder newSql = new StringBuilder (originalSql); StringBuilder injectedSql = new StringBuilder (); List<ParameterMapping> injectedMappings = new ArrayList <>(); Configuration configuration = ms.getConfiguration(); for (Map.Entry<String, Object> entry : additionalParams.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); injectedSql.append(key).append(" = ?, " ); injectedMappings.add(new ParameterMapping .Builder(configuration, key, value.getClass()).build()); boundSql.setAdditionalParameter(key, value); } newSql.insert(setIndex + 5 , injectedSql); List<ParameterMapping> finalMappings = new ArrayList <>(injectedMappings); finalMappings.addAll(boundSql.getParameterMappings()); metaObj.setValue("delegate.boundSql.sql" , newSql.toString()); metaObj.setValue("delegate.boundSql.parameterMappings" , finalMappings); return invocation.proceed(); } @Override public Object plugin (Object target) { return target instanceof StatementHandler ? Plugin.wrap(target, this ) : target; } }
这个方案主要是维护一个不填充审计字段的”黑名单”,在拦截逻辑第一步检查,新增或去掉不需要审计的表,只要往集合里加/删就行,维护成本也低。
也有人可能会觉得,在我们能确认数据来源是绝对安全的时候,ParameterMapping
其实并没有必要,增加创建开销,属于是吃力不讨好了,那笔者再写一个纯字符串拼接的方案
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 import com.baomidou.mybatisplus.core.toolkit.PluginUtils;import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;import org.apache.ibatis.executor.statement.StatementHandler;import org.apache.ibatis.mapping.*;import org.apache.ibatis.plugin.*;import org.apache.ibatis.reflection.MetaObject;import org.apache.ibatis.reflection.SystemMetaObject;import org.springframework.stereotype.Component;import java.sql.Connection;import java.util.*;@Component @Intercepts({@Signature( type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} )}) public class UpdateSqlInterceptor extends AbstractSqlParserHandler implements Interceptor { @Override public Object intercept (Invocation invocation) throws Throwable { StatementHandler handler = PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObj = SystemMetaObject.forObject(handler); this .sqlParser(metaObj); MappedStatement ms = (MappedStatement) metaObj.getValue("delegate.mappedStatement" ); if (ms.getSqlCommandType() != SqlCommandType.UPDATE || ms.getStatementType() == StatementType.CALLABLE) { return invocation.proceed(); } BoundSql boundSql = (BoundSql) metaObj.getValue("delegate.boundSql" ); String sql = boundSql.getSql(); if (!sql.toUpperCase().contains(" SET " )) { return invocation.proceed(); } List<ParameterMapping> mappings = new ArrayList <>(boundSql.getParameterMappings()); Map<String, Object> params = new HashMap <>(2 ); params.put("update_by" , UserUtils.getCurrentUserId()); params.put("update_date" , DateUtils.formatDateTime(new Date ())); for (Map.Entry<String, Object> map : params.entrySet()) { if (!sql.contains(map.getKey())) { sql = sql.replace("SET" , "SET " + map.getKey() + " = '" + map.getValue() + "'," ); } } metaObj.setValue("delegate.boundSql.sql" , sql); metaObj.setValue("delegate.boundSql.parameterMappings" , mappings); return invocation.proceed(); } @Override public Object plugin (Object target) { return target instanceof StatementHandler ? Plugin.wrap(target, this ) : target; } }
当你执行了如下 SQL
1 UPDATE table SET update_date = '2025-07-20 23:58:31' , update_by = '1' , code= ? WHERE del_flag= 0 AND (id = ?)
参数信息示例:
1 ==> Parameters: 121253(String), 23(String)
虽然字符串拼接 replace
方法几乎没有额外开销,性能最好,但不支持类型自动绑定(如 Date、Long 转换),笔者个人还是比较推荐 ParameterMapping
+ BoundSql.setAdditionalParameter
的方案。该方案和 MyBatis 参数机制一致,插入参数可控,类型自动绑定,性能损耗上也仅是增加了构建参数和拼接 SQL 的开销。
总结 通过本次对 updateFill
严格模式失效、LambdaUpdateWrapper
更新未触发字段填充以及 removeById
删除操作忽略自动填充这三大疑难场景的剖析,相信笔者和读者都对这个框架有了更深的认知和理解,当然没有最好的方案,只有最适合的方案 ,希望这些经验能帮助你在实际项目中少走弯路!
技术之路,从理解“为什么失效”开始,以掌握“如何创造”为终。