目录
- 前言
- 1. 源码及API
- 2. Demo架构
- 3. 全局字段填充(实战)
- 4. 局部字段不填充(实战)
前言
对于Java的相关知识推荐阅读:
- java框架 零基础从入门到精通的学习路线 附开源项目面经等(超全)
- 【Java项目】实战CRUD的功能整理(持续更新)
MetaObjectHandler 是 MyBatis-Plus 提供的一个接口,用于处理在插入和更新操作时的一些公共字段的自动填充
比如,在插入记录时自动填充 createTime
字段,在更新记录时自动填充 updateTime
字段
(一开始实战过程中莫名其妙的填充了这两个数据值,但是查找数据库触发器确没有这个,好奇哪里触发的,给我上了一课,对此详细研究并且科普该知识点)
1. 源码及API
通过源码分析各个方法:
/*
* Copyright (c) 2011-2023, baomidou (jobob@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.baomidou.mybatisplus.core.handlers;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import org.apache.ibatis.reflection.MetaObject;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
/**
* 元对象字段填充控制器抽象类,实现公共字段自动写入<p>
* <p>
* 所有入参的 MetaObject 必定是 entity 或其子类的 MetaObject
*
* @author hubin
* @since 2016-08-28
*/
public interface MetaObjectHandler {
/**
* 是否开启了插入填充
*/
default boolean openInsertFill() {
return true;
}
/**
* 是否开启了更新填充
*/
default boolean openUpdateFill() {
return true;
}
/**
* 插入元对象字段填充(用于插入时对公共字段的填充)
*
* @param metaObject 元对象
*/
void insertFill(MetaObject metaObject);
/**
* 更新元对象字段填充(用于更新时对公共字段的填充)
*
* @param metaObject 元对象
*/
void updateFill(MetaObject metaObject);
/**
* 通用填充
*
* @param fieldName java bean property name
* @param fieldVal java bean property value
* @param metaObject meta object parameter
*/
default MetaObjectHandler setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject) {
if (Objects.nonNull(fieldVal) && metaObject.hasSetter(fieldName)) {
metaObject.setValue(fieldName, fieldVal);
}
return this;
}
/**
* get value from java bean by propertyName
*
* @param fieldName java bean property name
* @param metaObject parameter wrapper
* @return 字段值
*/
default Object getFieldValByName(String fieldName, MetaObject metaObject) {
return metaObject.hasGetter(fieldName) ? metaObject.getValue(fieldName) : null;
}
/**
* find the tableInfo cache by metaObject </p>
* 获取 TableInfo 缓存
*
* @param metaObject meta object parameter
* @return TableInfo
* @since 3.3.0
*/
default TableInfo findTableInfo(MetaObject metaObject) {
return TableInfoHelper.getTableInfo(metaObject.getOriginalObject().getClass());
}
/**
* @param metaObject metaObject meta object parameter
* @return this
* @since 3.3.0
*/
default <T, E extends T> MetaObjectHandler strictInsertFill(MetaObject metaObject, String fieldName, Class<T> fieldType, E fieldVal) {
return strictInsertFill(findTableInfo(metaObject), metaObject, Collections.singletonList(StrictFill.of(fieldName, fieldType, fieldVal)));
}
/**
* @param metaObject metaObject meta object parameter
* @return this
* @since 3.3.0
*/
default <T, E extends T> MetaObjectHandler strictInsertFill(MetaObject metaObject, String fieldName, Supplier<E> fieldVal, Class<T> fieldType) {
return strictInsertFill(findTableInfo(metaObject), metaObject, Collections.singletonList(StrictFill.of(fieldName, fieldVal, fieldType)));
}
/**
* @param metaObject metaObject meta object parameter
* @return this
* @since 3.3.0
*/
default MetaObjectHandler strictInsertFill(TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) {
return strictFill(true, tableInfo, metaObject, strictFills);
}
/**
* @param metaObject metaObject meta object parameter
* @return this
* @since 3.3.0
*/
default <T, E extends T> MetaObjectHandler strictUpdateFill(MetaObject metaObject, String fieldName, Supplier<E> fieldVal, Class<T> fieldType) {
return strictUpdateFill(findTableInfo(metaObject), metaObject, Collections.singletonList(StrictFill.of(fieldName, fieldVal, fieldType)));
}
/**
* @param metaObject metaObject meta object parameter
* @return this
* @since 3.3.0
*/
default <T, E extends T> MetaObjectHandler strictUpdateFill(MetaObject metaObject, String fieldName, Class<T> fieldType, E fieldVal) {
return strictUpdateFill(findTableInfo(metaObject), metaObject, Collections.singletonList(StrictFill.of(fieldName, fieldType, fieldVal)));
}
/**
* @param metaObject metaObject meta object parameter
* @return this
* @since 3.3.0
*/
default MetaObjectHandler strictUpdateFill(TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) {
return strictFill(false, tableInfo, metaObject, strictFills);
}
/**
* 严格填充,只针对非主键的字段,只有该表注解了fill 并且 字段名和字段属性 能匹配到才会进行填充(null 值不填充)
*
* @param insertFill 是否验证在 insert 时填充
* @param tableInfo cache 缓存
* @param metaObject metaObject meta object parameter
* @param strictFills 填充信息
* @return this
* @since 3.3.0
*/
default MetaObjectHandler strictFill(boolean insertFill, TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) {
if ((insertFill && tableInfo.isWithInsertFill()) || (!insertFill && tableInfo.isWithUpdateFill())) {
strictFills.forEach(i -> {
final String fieldName = i.getFieldName();
final Class<?> fieldType = i.getFieldType();
tableInfo.getFieldList().stream()
.filter(j -> j.getProperty().equals(fieldName) && fieldType.equals(j.getPropertyType()) &&
((insertFill && j.isWithInsertFill()) || (!insertFill && j.isWithUpdateFill()))).findFirst()
.ifPresent(j -> strictFillStrategy(metaObject, fieldName, i.getFieldVal()));
});
}
return this;
}
/**
* 填充策略,默认有值不覆盖,如果提供的值为null也不填充
*
* @param metaObject metaObject meta object parameter
* @param fieldName java bean property name
* @param fieldVal java bean property value of Supplier
* @return this
* @since 3.3.0
*/
default MetaObjectHandler fillStrategy(MetaObject metaObject, String fieldName, Object fieldVal) {
if (getFieldValByName(fieldName, metaObject) == null) {
setFieldValByName(fieldName, fieldVal, metaObject);
}
return this;
}
/**
* 严格模式填充策略,默认有值不覆盖,如果提供的值为null也不填充
*
* @param metaObject metaObject meta object parameter
* @param fieldName java bean property name
* @param fieldVal java bean property value of Supplier
* @return this
* @since 3.3.0
*/
default MetaObjectHandler strictFillStrategy(MetaObject metaObject, String fieldName, Supplier<?> fieldVal) {
if (metaObject.getValue(fieldName) == null) {
Object obj = fieldVal.get();
if (Objects.nonNull(obj)) {
metaObject.setValue(fieldName, obj);
}
}
return this;
}
}
对应的代码都较为简单,此处以API的概念属性以及如何调用进行讲解
方法名 | 概念 | 作用 | 参数 | 调用方式 |
---|---|---|---|---|
openInsertFill() | 检查是否开启插入填充 | 返回布尔值,表示是否在插入时进行字段填充 | 无 | boolean open = metaObjectHandler.openInsertFill(); |
openUpdateFill() | 检查是否开启更新填充 | 返回布尔值,表示是否在更新时进行字段填充 | 无 | boolean open = metaObjectHandler.openUpdateFill(); |
insertFill(MetaObject metaObject) | 插入操作时填充元对象字段 | 定义插入时的字段自动填充逻辑 | MetaObject metaObject: 元对象 | metaObjectHandler.insertFill(metaObject); |
updateFill(MetaObject metaObject) | 更新操作时填充元对象字段 | 定义更新时的字段自动填充逻辑 | MetaObject metaObject: 元对象 | metaObjectHandler.updateFill(metaObject); |
setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject) | 通用字段填充方法 | 根据字段名设置字段值 | String fieldName: 字段名 Object fieldVal: 字段值 MetaObject metaObject: 元对象 | metaObjectHandler.setFieldValByName(“fieldName”, fieldVal, metaObject); |
getFieldValByName(String fieldName, MetaObject metaObject) | 获取字段值 | 根据字段名获取字段值 | String fieldName: 字段名 MetaObject metaObject: 元对象 | Object value = metaObjectHandler.getFieldValByName(“fieldName”, metaObject); |
findTableInfo(MetaObject metaObject) | 获取表信息 | 根据元对象查找表信息缓存 | MetaObject metaObject: 元对象 | TableInfo tableInfo = metaObjectHandler.findTableInfo(metaObject); |
strictInsertFill(MetaObject metaObject, String fieldName, Class fieldType, E fieldVal) | 严格模式插入填充 | 在插入操作时严格填充指定字段 | MetaObject metaObject: 元对象 String fieldName: 字段名 Class fieldType: 字段类型 E fieldVal: 字段值 | metaObjectHandler.strictInsertFill(metaObject, “fieldName”, fieldType, fieldVal); |
strictInsertFill(MetaObject metaObject, String fieldName, Supplier fieldVal, Class fieldType) | 严格模式插入填充 | 在插入操作时严格填充指定字段 | MetaObject metaObject: 元对象 String fieldName: 字段名 Supplier fieldVal: 字段值提供者 Class fieldType: 字段类型 | metaObjectHandler.strictInsertFill(metaObject, “fieldName”, fieldVal, fieldType); |
strictInsertFill(TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) | 严格模式插入填充 | 在插入操作时严格填充指定字段 | TableInfo tableInfo: 表信息 MetaObject metaObject: 元对象 List<StrictFill<?, ?>> strictFills: 填充信息列表 | metaObjectHandler.strictInsertFill(tableInfo, metaObject, strictFills); |
strictUpdateFill(MetaObject metaObject, String fieldName, Supplier fieldVal, Class fieldType) | 严格模式更新填充 | 在更新操作时严格填充指定字段 | MetaObject metaObject: 元对象 String fieldName: 字段名 Supplier fieldVal: 字段值提供者 Class fieldType: 字段类型 | metaObjectHandler.strictUpdateFill(metaObject, “fieldName”, fieldVal, fieldType); |
strictUpdateFill(MetaObject metaObject, String fieldName, Class fieldType, E fieldVal) | 严格模式更新填充 | 在更新操作时严格填充指定字段 | MetaObject metaObject: 元对象 String fieldName: 字段名 Class fieldType: 字段类型 E fieldVal: 字段值 | metaObjectHandler.strictUpdateFill(metaObject, “fieldName”, fieldType, fieldVal); |
strictUpdateFill(TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) | 严格模式更新填充 | 在更新操作时严格填充指定字段 | TableInfo tableInfo: 表信息 MetaObject metaObject: 元对象 List<StrictFill<?, ?>> strictFills: 填充信息列表 | metaObjectHandler.strictUpdateFill(tableInfo, metaObject, strictFills); |
strictFill(boolean insertFill, TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) | 严格填充策略 | 根据表信息和元对象进行严格的插入或更新填充 | boolean insertFill: 插入/更新标志 TableInfo tableInfo: 表信息 MetaObject metaObject: 元对象 List<StrictFill<?, ?>> strictFills: 填充信息列表 | metaObjectHandler.strictFill(insertFill, tableInfo, metaObject, strictFills); |
fillStrategy(MetaObject metaObject, String fieldName, Object fieldVal) | 填充策略 | 根据策略填充字段值,默认是字段值为null时才填充 | MetaObject metaObject: 元对象 String fieldName: 字段名 Object fieldVal: 字段值 | metaObjectHandler.fillStrategy(metaObject, “fieldName”, fieldVal); |
strictFillStrategy(MetaObject metaObject, String fieldName, Supplier<?> fieldVal) | 严格模式填充策略 | 严格填充字段值,只有在当前字段值为null时才填充 | MetaObject metaObject: 元对象 String fieldName: 字段名 Supplier<?> fieldVal: 字段值提供者 | metaObjectHandler.strictFillStrategy(metaObject, “fieldName”, fieldVal); |
方法包括检查是否开启填充、具体的填充实现、字段值的设置和获取策略等
2. Demo架构
具体的示例如下:
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
System.out.println("start insert fill ....");
// 使用 strictInsertFill 方法插入时间字段
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
System.out.println("start update fill ....");
// 使用 strictUpdateFill 方法更新时间字段
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
常用的填充方法如下:
strictInsertFill
: 严格模式下的插入填充,只有在字段为 null 时才会填充,避免覆盖已有值strictUpdateFill
: 严格模式下的更新填充,只有在字段为 null 时才会填充,避免覆盖已有值fillStrategy
::指定填充策略,可选值包括 NOT_NULL(仅当字段为 null 时填充)和 DEFAULT(无条件填充)
对应的实体类如下:
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
// getters and setters
}
上述注解的说明如下(源码说明):
public enum FieldFill {
/**
* 默认不处理
*/
DEFAULT,
/**
* 插入时填充字段
*/
INSERT,
/**
* 更新时填充字段
*/
UPDATE,
/**
* 插入和更新时填充字段
*/
INSERT_UPDATE
}
对应还需要增加一个配置类:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MyMetaObjectHandler();
}
}
最终执行插入操作并更新:
User user = new User();
userMapper.insert(user);
// user.getCreateTime() 会被自动填充
userMapper.updateById(user);
此种情况下自动填充属性会失效
-
字段未使用 @TableField 注解:未使用 @TableField 注解或 fill 属性未设置为
FieldFill.INSERT
或FieldFill.UPDATE
,自动注入将不会生效 -
未实现 MetaObjectHandler 接口:MyBatis-Plus 将不知道如何处理自动填充逻辑。
-
未在配置类中注册 MetaObjectHandler:即使实现了 MetaObjectHandler 接口,如果未在 Spring 配置类中进行注册,自动填充也不会生效
3. 全局字段填充(实战)
一些开源项目经常会让我们在创建表的过程中需要保留一些字段,究其原因是属性的自动填充
对于项目中
public class DefaultDBFieldHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
LocalDateTime current = LocalDateTime.now();
// 创建时间为空,则以当前时间为插入时间
if (Objects.isNull(baseDO.getCreateTime())) {
baseDO.setCreateTime(current);
}
// 更新时间为空,则以当前时间为更新时间
if (Objects.isNull(baseDO.getUpdateTime())) {
baseDO.setUpdateTime(current);
}
}
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新时间为空,则以当前时间为更新时间
Object modifyTime = getFieldValByName("updateTime", metaObject);
if (Objects.isNull(modifyTime)) {
setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
}
}
其中BaseDO是我的一些固有属性类:
@Data
@JsonIgnoreProperties(value = "transMap") // 避免 Jackson 在 Spring Cache 反序列化报错
public abstract class BaseDO implements Serializable, TransPojo {
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 最后更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 创建者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
private String creator;
/**
* 更新者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
private String updater;
/**
* 是否删除
*/
@TableLogic
private Boolean deleted;
}
同步增加其配置类:
public class MybatisAutoConfiguration {
@Bean
public MetaObjectHandler defaultMetaObjectHandler(){
return new DefaultDBFieldHandler(); // 自动填充参数类
}
}
对应正式的功能如下:
@Service
@Validated
public class GoodsStoragePlanServiceImpl implements GoodsStoragePlanService {
@Resource
private GoodsStoragePlanMapper goodsStoragePlanMapper;
@Resource
private AdminUserService userService;
@Override
public Long createGoodsStoragePlan(GoodsStoragePlanSaveReqVO createReqVO) {
// 插入
GoodsStoragePlanDO goodsStoragePlan = BeanUtils.toBean(createReqVO, GoodsStoragePlanDO.class);
goodsStoragePlanMapper.insert(goodsStoragePlan);
}
}
后续便会自动填充相应的属性
4. 局部字段不填充(实战)
代码和第三章的实战差不多,在上面代码的改进
需求:
上面的创建是增加时间和修改时间都会被自动注入
由于我这个只需要增加时间自动注入,修改时间只有在创建的时候自动注入,创建的时候需要为空
增加临时禁用全局字段的填充机制:引入一个上下文变量来控制是否启用字段填充
- 创建一个 ThreadLocal 变量来控制是否启用字段填充:
ThreadLocal 变量为每个线程提供了独立的变量副本,从而保证了线程安全
每个线程可以独立地访问其 ThreadLocal 变量副本,而不会影响其他线程
/**
* FieldFillContext 类用于控制全局字段填充机制。
* 使用 ThreadLocal 变量来保证线程安全,实现线程独立的变量副本。
*/
public class FieldFillContext {
/**
* ThreadLocal 变量,用于存储每个线程是否启用字段填充的状态。
* 初始值为 true,即默认启用字段填充。
*/
private static final ThreadLocal<Boolean> ENABLE_FIELD_FILL = ThreadLocal.withInitial(() -> true);
/**
* 设置当前线程是否启用字段填充。
*
* @param enable 如果为 true,则启用字段填充;如果为 false,则禁用字段填充。
*/
public static void setEnableFieldFill(boolean enable) {
ENABLE_FIELD_FILL.set(enable);
}
/**
* 获取当前线程是否启用字段填充的状态。
*
* @return 如果启用字段填充,则返回 true;否则返回 false。
*/
public static boolean isEnableFieldFill() {
return ENABLE_FIELD_FILL.get();
}
/**
* 清除当前线程的 ThreadLocal 变量,防止内存泄漏。
*/
public static void clear() {
ENABLE_FIELD_FILL.remove();
}
}
- 修改 MetaObjectHandler 使用上下文变量
在 MetaObjectHandler 中检查上下文变量的值,决定是否进行字段填充:
public class DefaultDBFieldHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
if (!FieldFillContext.isEnableFieldFill()) {
return;
}
/**
* 以下没变动
*/
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
LocalDateTime current = LocalDateTime.now();
// 创建时间为空,则以当前时间为插入时间
if (Objects.isNull(baseDO.getCreateTime())) {
baseDO.setCreateTime(current);
}
// 更新时间为空,则以当前时间为更新时间
if (Objects.isNull(baseDO.getUpdateTime())) {
baseDO.setUpdateTime(current);
}
}
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新时间为空,则以当前时间为更新时间
Object modifyTime = getFieldValByName("updateTime", metaObject);
if (Objects.isNull(modifyTime)) {
setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
}
}
对应功能如下:
@Service
@Validated
public class GoodsStoragePlanServiceImpl implements GoodsStoragePlanService {
@Resource
private GoodsStoragePlanMapper goodsStoragePlanMapper;
@Resource
private AdminUserService userService;
@Override
public Long createGoodsStoragePlan(GoodsStoragePlanSaveReqVO createReqVO) {
// 插入
GoodsStoragePlanDO goodsStoragePlan = BeanUtils.toBean(createReqVO, GoodsStoragePlanDO.class);
goodsStoragePlan.setCreateTime(LocalDateTime.now());
goodsStoragePlan.setUpdateTime(null);
// 禁用全局字段填充
FieldFillContext.setEnableFieldFill(false);
try {
goodsStoragePlanMapper.insert(goodsStoragePlan);
} finally {
// 恢复全局字段填充
FieldFillContext.clear();
}
// 返回
return goodsStoragePlan.getId();
}
}