news 2026/4/16 15:51:47

MyBatis-Plus自动填充字段:优雅处理创建时间和更新时间

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatis-Plus自动填充字段:优雅处理创建时间和更新时间

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开始,推荐使用strictInsertFillstrictUpdateFill方法,它们会进行严格的类型检查,避免类型不匹配的问题。

四、完整示例:用户管理场景

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.1‌MyBatis 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) { // 当前实现为空,可根据需要添加属性配置逻辑 } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 12:21:01

如何在Mac上轻松创建Windows 11安装镜像:CrystalFetch完整指南

如何在Mac上轻松创建Windows 11安装镜像&#xff1a;CrystalFetch完整指南 【免费下载链接】CrystalFetch macOS UI for creating Windows installer ISO from UUPDump 项目地址: https://gitcode.com/gh_mirrors/cr/CrystalFetch 对于Mac用户来说&#xff0c;想要在苹果…

作者头像 李华
网站建设 2026/4/16 12:21:10

各地人才认定新规不断,技术人才如何用几件软著“补齐一块短板”?

各地人才认定新规不断&#xff0c;技术人才如何用几件软著“补齐一块短板”&#xff1f; 最近不少城市升级了高层次人才、青年人才、数字人才的认定办法&#xff0c;给技术人员提供了落户、补贴、购房、子女入学等一系列利好。 很多程序员、架构师会遇到一个共同问题&#xf…

作者头像 李华
网站建设 2026/4/16 12:20:48

编程考级科学体系:全学段覆盖的科目有哪些

编程考级科学体系:全学段覆盖的科目有哪些 学习路径概述 从专业视角分析,青少年编程学习与能力认证可分为三个关键阶段。第一阶段侧重于兴趣启蒙与基础认知,旨在通过信息素养和图形化编程等内容,帮助孩子建立计算思维,避免因过早接触抽象语法而影响学习热情。第二阶段是…

作者头像 李华
网站建设 2026/4/16 11:27:22

2026海外代理IP服务横向测评,谁是你的最终选择?

01 核心差异对比在深入分析之前&#xff0c;先通过表格直观了解三大代理IP服务商的核心差异&#xff1a;维度对比IPIDEA全民HTTPLoongProxy核心IP类型动态住宅代理、静态住宅代理、移动代理、数据中心代理国内动态/静态IP、隧道代理IP、独享代理IP静态住宅ISP、静态数据中心IP、…

作者头像 李华
网站建设 2026/4/15 17:36:45

OpenCourseCatalog:构建终身学习导航系统的终极指南

OpenCourseCatalog&#xff1a;构建终身学习导航系统的终极指南 【免费下载链接】OpenCourseCatalog Bilibili 公开课目录 项目地址: https://gitcode.com/gh_mirrors/op/OpenCourseCatalog 在知识爆炸的时代&#xff0c;你是否曾面临这样的困境&#xff1a;面对海量学习…

作者头像 李华
网站建设 2026/4/16 15:24:46

Signal-Android隐私通讯应用开发实战指南

Signal-Android隐私通讯应用开发实战指南 【免费下载链接】Signal-Android A private messenger for Android. 项目地址: https://gitcode.com/GitHub_Trending/si/Signal-Android Signal-Android作为一款专注于隐私保护的即时通讯应用&#xff0c;在开发过程中展现了卓…

作者头像 李华