MyBatis-Plus 提供了一个便捷的自动填充功能,用于在插入或更新数据时自动填充某些字段,如创建时间、更新时间等。
官方文档:https://baomidou.com/guides/auto-fill-field/
一、引言
在日常开发中,我们经常需要处理一些公共字段的自动填充,比如数据的创建时间、更新时间、创建人、更新人等。手动为这些字段赋值不仅繁琐,而且容易遗漏。MyBatis-Plus作为MyBatis的增强工具,提供了强大的自动填充功能,让我们能够优雅地解决这个问题。
二、什么是自动填充?
自动填充是MyBatis-Plus的一个核心功能,它允许我们在执行插入或更新操作时,自动为特定字段填充值。最常见的应用场景包括:
创建时间:数据插入时自动填充当前时间
更新时间:数据更新时自动填充当前时间
操作人信息:自动填充当前登录用户ID或姓名
逻辑删除标记:自动填充删除状态
三、实现方式详解
MyBatis-Plus提供了两种实现自动填充的方式:注解配置和自定义处理器。
3.1方式一:使用@TableField注解(简单场景)
对于简单的固定值填充,可以直接在实体类字段上使用@TableField注解:
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.TableField; import java.time.LocalDateTime; public class User { private Long id; private String name; // 插入时自动填充固定值 @TableField(fill = FieldFill.INSERT, value = "'system'") private String createBy; // 插入和更新时自动填充固定值 @TableField(fill = FieldFill.INSERT_UPDATE, value = "'admin'") private String updateBy; }支持的操作类型:
FieldFill.DEFAULT:默认不处理FieldFill.INSERT:插入时填充FieldFill.UPDATE:更新时填充FieldFill.INSERT_UPDATE:插入和更新时都填充
3.2方式二:实现MetaObjectHandler接口(推荐)
对于需要动态计算的复杂场景,实现MetaObjectHandler接口是更灵活的选择:
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @Component public class MyMetaObjectHandler implements MetaObjectHandler { /** * 插入时自动填充 */ @Override public void insertFill(MetaObject metaObject) { // 填充创建时间 this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 填充更新时间(插入时也需要) this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 填充创建人(从线程上下文或SecurityContext中获取) String currentUser = getCurrentUser(); this.strictInsertFill(metaObject, "createBy", String.class, currentUser); // 填充更新人 this.strictInsertFill(metaObject, "updateBy", String.class, currentUser); // 填充其他业务字段 this.strictInsertFill(metaObject, "tenantId", Long.class, getTenantId()); } /** * 更新时自动填充 */ @Override public void updateFill(MetaObject metaObject) { // 只填充更新时间 this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 填充更新人 String currentUser = getCurrentUser(); this.strictUpdateFill(metaObject, "updateBy", String.class, currentUser); } /** * 获取当前用户(示例方法) */ private String getCurrentUser() { // 实际项目中可以从SecurityContext、JWT token或ThreadLocal中获取 return "admin"; } /** * 获取租户ID(多租户场景) */ private Long getTenantId() { return 1L; } }注意:从MyBatis-Plus 3.3.0开始,推荐使用strictInsertFill和strictUpdateFill方法,它们会进行严格的类型检查,避免类型不匹配的问题。
四、完整示例:用户管理场景
4.1 实体类定义
import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("sys_user") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String email; private Integer status; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT) private String createBy; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableField(fill = FieldFill.INSERT_UPDATE) private String updateBy; @TableLogic @TableField(fill = FieldFill.INSERT) private Integer deleted; }4.2 Mapper接口
import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface UserMapper extends BaseMapper<User> { }4.3 自定义填充处理器
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @Slf4j @Component public class AutoFillMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { log.info("开始插入填充..."); // 方法1: 根据属性类型自动匹配(推荐) this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 方法2: 直接设置值(不推荐,缺少类型检查) // this.setFieldValByName("createTime", LocalDateTime.now(), metaObject); // 从ThreadLocal获取当前用户(示例) UserContext currentUser = UserContextHolder.get(); if (currentUser != null) { this.strictInsertFill(metaObject, "createBy", String.class, currentUser.getUsername()); this.strictInsertFill(metaObject, "updateBy", String.class, currentUser.getUsername()); } // 逻辑删除字段默认值 this.strictInsertFill(metaObject, "deleted", Integer.class, 0); } @Override public void updateFill(MetaObject metaObject) { log.info("开始更新填充..."); this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); UserContext currentUser = UserContextHolder.get(); if (currentUser != null) { this.strictUpdateFill(metaObject, "updateBy", String.class, currentUser.getUsername()); } } } // 用户上下文持有器(示例) class UserContextHolder { private static final ThreadLocal<UserContext> holder = new ThreadLocal<>(); public static void set(UserContext userContext) { holder.set(userContext); } public static UserContext get() { return holder.get(); } public static void clear() { holder.remove(); } } class UserContext { private String username; private Long userId; // getters and setters }4.4 业务层使用
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override @Transactional public boolean saveUser(User user) { // 设置当前用户上下文(通常在拦截器中设置) UserContext context = new UserContext(); context.setUsername("currentUser"); UserContextHolder.set(context); try { // 插入操作,自动填充字段会自动处理 return this.save(user); } finally { // 清理线程变量 UserContextHolder.clear(); } } @Override @Transactional public boolean updateUser(User user) { UserContext context = new UserContext(); context.setUsername("currentUser"); UserContextHolder.set(context); try { // 更新操作,自动填充字段会自动处理 return this.updateById(user); } finally { UserContextHolder.clear(); } } }五、高级用法和注意事项
5.1 条件填充
有时我们需要根据条件决定是否填充:
@Override public void insertFill(MetaObject metaObject) { // 只有字段为空时才填充 if (metaObject.getValue("createTime") == null) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); } // 根据实体类的某个属性决定是否填充 User user = (User) metaObject.getOriginalObject(); if (user != null && "admin".equals(user.getUserType())) { this.strictInsertFill(metaObject, "adminFlag", String.class, "Y"); } }5.2 多租户场景
@Override public void insertFill(MetaObject metaObject) { // 自动填充租户ID Long tenantId = TenantContext.getCurrentTenantId(); if (tenantId != null) { this.strictInsertFill(metaObject, "tenantId", Long.class, tenantId); } }5.3 注意事项
字段默认值冲突:如果数据库字段有默认值,且实体类字段也有自动填充,可能会产生冲突
批量操作:批量插入和更新同样支持自动填充
局部更新:使用
update(Wrapper<T> updateWrapper)时,自动填充仍然生效性能考虑:自动填充会增加一定的性能开销,但对于大多数应用来说可以忽略不计
六、 常见问题解决
问题1:填充不生效
检查
MetaObjectHandler是否被Spring管理(添加@Component注解)检查实体类字段是否添加了
@TableField(fill = ...)注解检查字段名是否与
strictInsertFill方法中的参数一致
问题2:类型转换错误
确保填充值的类型与实体类字段类型一致
使用
strictInsertFill方法进行严格的类型检查
问题3:填充时机不对
插入操作触发
insertFill方法更新操作触发
updateFill方法使用
saveOrUpdate方法时,会根据记录是否存在决定调用哪个方法
七、工作实战
7.1MyBatis Plus自动填充
package com.zm.platform.framework.handler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.zm.platform.constant.ValidFlagConstant; import com.zm.platform.modular.loginUser.service.LoginUserService; import com.zm.platform.modular.user.entity.User; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import java.util.Date; import java.util.Objects; @Slf4j @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Lazy @Autowired private LoginUserService loginUserService; @Override public void insertFill(MetaObject metaObject) { log.info("start insert fill ...."); User user = new User(); try { user = loginUserService.getLoginUser().getUser(); } catch (Exception e) { } this.fillStrategy(metaObject, "createName", user.getRealName()); this.fillStrategy(metaObject, "createId", user.getId()); this.fillStrategy(metaObject, "createTime", new Date()); this.fillStrategy(metaObject, "valiflag", ValidFlagConstant.EFFECTIVE); this.fillStrategy(metaObject, "orgId", user.getOrgId()); this.fillStrategy(metaObject, "orgName", user.getOrgName()); this.fillStrategy(metaObject, "updateName", user.getRealName()); this.fillStrategy(metaObject, "updateId", user.getId()); this.fillStrategy(metaObject, "updtId", user.getId()); this.fillStrategy(metaObject, "updtName", user.getRealName()); this.fillStrategy(metaObject, "updateTime", new Date()); this.fillStrategy(metaObject, "updtTime", new Date()); // this.setFieldValByName("createName", user.getRealName() ,metaObject); // this.setFieldValByName( "createId", user.getId() ,metaObject); // this.setFieldValByName("orgId",user.getOrgId() ,metaObject); // this.setFieldValByName("orgName",user.getOrgName() ,metaObject); // this.setFieldValByName( "createTime", new Date() ,metaObject); // this.setFieldValByName( "valiflag", ValidFlagConstant.EFFECTIVE ,metaObject); } @Override public void updateFill(MetaObject metaObject) { log.info("start update fill ...."); User user = new User(); try { user = loginUserService.getLoginUser().getUser(); } catch (Exception e) { } this.setFieldValByName("updateName", user.getRealName(), metaObject); this.setFieldValByName("updateId", user.getId(), metaObject); this.setFieldValByName("updtId", user.getId(), metaObject); this.setFieldValByName("updtName", user.getRealName(), metaObject); this.setFieldValByName("updateTime", new Date(), metaObject); this.setFieldValByName("updtTime", new Date(), metaObject); } }7.2MyBatis自动填充
package com.jingdianjichi.subject.infra.config; import com.jingdianjichi.subject.common.util.LoginUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.*; import org.springframework.stereotype.Component; import java.lang.reflect.Field; import java.util.*; /** * MyBatis拦截器,用于自动填充实体类中的创建人、创建时间、更新人、更新时间等公共字段 * 在执行INSERT或UPDATE操作时自动设置相关字段值 */ @Component @Slf4j @Intercepts({@Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class })}) public class MybatisInterceptor implements Interceptor { /** * 拦截方法,在MyBatis执行更新操作前被调用 * @param invocation 调用信息对象,包含被拦截的方法信息和参数 * @return 执行结果 * @throws Throwable 异常信息 */ @Override public Object intercept(Invocation invocation) throws Throwable { // 获取MappedStatement对象,包含SQL语句的信息 MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; // 获取SQL命令类型(INSERT、UPDATE、DELETE、SELECT等) SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); // 获取SQL参数对象 Object parameter = invocation.getArgs()[1]; // 如果参数为空,则直接执行原方法 if (parameter == null) { return invocation.proceed(); } // 获取当前登录用户的ID String loginId = LoginUtil.getLoginId(); // 如果用户未登录或登录ID为空,则直接执行原方法 if (StringUtils.isBlank(loginId)) { return invocation.proceed(); } // 如果是插入或更新操作,则处理实体属性填充 if (SqlCommandType.INSERT == sqlCommandType || SqlCommandType.UPDATE == sqlCommandType) { replaceEntityProperty(parameter, loginId, sqlCommandType); } // 继续执行原方法 return invocation.proceed(); } /** * 根据参数类型替换实体属性值 * @param parameter SQL参数对象 * @param loginId 当前登录用户ID * @param sqlCommandType SQL命令类型 */ private void replaceEntityProperty(Object parameter, String loginId, SqlCommandType sqlCommandType) { // 如果参数是Map类型,则遍历Map中的每个值进行处理 if (parameter instanceof Map) { replaceMap((Map) parameter, loginId, sqlCommandType); } else { // 如果不是Map类型,则直接处理该对象 replace(parameter, loginId, sqlCommandType); } } /** * 处理Map类型的参数 * @param parameter Map参数 * @param loginId 当前登录用户ID * @param sqlCommandType SQL命令类型 */ private void replaceMap(Map parameter, String loginId, SqlCommandType sqlCommandType) { // 遍历Map中的所有值,对每个值进行处理 for (Object val : parameter.values()) { replace(val, loginId, sqlCommandType); } } /** * 根据SQL命令类型决定处理方式 * @param parameter 参数对象 * @param loginId 当前登录用户ID * @param sqlCommandType SQL命令类型 */ private void replace(Object parameter, String loginId, SqlCommandType sqlCommandType) { // 如果是插入操作,则处理插入相关的字段 if (SqlCommandType.INSERT == sqlCommandType) { dealInsert(parameter, loginId); } else { // 如果是更新操作,则处理更新相关的字段 dealUpdate(parameter, loginId); } } /** * 处理更新操作,自动填充更新人和更新时间字段 * @param parameter 参数对象 * @param loginId 当前登录用户ID */ private void dealUpdate(Object parameter, String loginId) { // 获取对象的所有字段(包括父类的字段) Field[] fields = getAllFields(parameter); // 遍历所有字段 for (Field field : fields) { try { // 设置字段可访问(即使是private字段也可以访问) field.setAccessible(true); // 获取字段当前值 Object o = field.get(parameter); // 如果字段已经有值,则跳过不处理 if (Objects.nonNull(o)) { field.setAccessible(false); continue; } // 如果字段名为updateBy,则设置为当前登录用户ID if ("updateBy".equals(field.getName())) { field.set(parameter, loginId); field.setAccessible(false); } // 如果字段名为updateTime,则设置为当前时间 else if ("updateTime".equals(field.getName())) { field.set(parameter, new Date()); field.setAccessible(false); } // 其他字段关闭访问权限 else { field.setAccessible(false); } } catch (Exception e) { // 记录错误日志 log.error("处理更新操作时发生错误:{}", e.getMessage(), e); } } } /** * 处理插入操作,自动填充创建人、创建时间、删除标识等字段 * @param parameter 参数对象 * @param loginId 当前登录用户ID */ private void dealInsert(Object parameter, String loginId) { // 获取对象的所有字段(包括父类的字段) Field[] fields = getAllFields(parameter); // 遍历所有字段 for (Field field : fields) { try { // 设置字段可访问(即使是private字段也可以访问) field.setAccessible(true); // 获取字段当前值 Object o = field.get(parameter); // 如果字段已经有值,则跳过不处理 if (Objects.nonNull(o)) { field.setAccessible(false); continue; } // 如果字段名为isDeleted(删除标识),则设置为0(未删除) if ("isDeleted".equals(field.getName())) { field.set(parameter, 0); field.setAccessible(false); } // 如果字段名为createdBy(创建人),则设置为当前登录用户ID else if ("createdBy".equals(field.getName())) { field.set(parameter, loginId); field.setAccessible(false); } // 如果字段名为createdTime(创建时间),则设置为当前时间 else if ("createdTime".equals(field.getName())) { field.set(parameter, new Date()); field.setAccessible(false); } // 其他字段关闭访问权限 else { field.setAccessible(false); } } catch (Exception e) { // 记录错误日志 log.error("处理插入操作时发生错误:{}", e.getMessage(), e); } } } /** * 获取对象的所有字段,包括父类中的字段 * @param object 对象实例 * @return 对象的所有字段数组 */ private Field[] getAllFields(Object object) { // 获取对象的Class Class<?> clazz = object.getClass(); // 创建字段列表 List<Field> fieldList = new ArrayList<>(); // 循环获取当前类及其父类的所有字段 while (clazz != null) { // 将当前类声明的所有字段添加到列表中 fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields()))); // 获取父类继续循环 clazz = clazz.getSuperclass(); } // 将字段列表转换为数组并返回 Field[] fields = new Field[fieldList.size()]; fieldList.toArray(fields); return fields; } /** * 插件包装方法 * @param target 目标对象 * @return 包装后的对象 */ @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 设置插件属性 * @param properties 属性配置 */ @Override public void setProperties(Properties properties) { // 当前实现为空,可根据需要添加属性配置逻辑 } }