前言
最近在工作中,接触到一个需求。系统中所有的表,都有一些固定的公共字段,例如:每张表中 都会有 id(主键)、status(逻辑删除)、create_time(数据添加时间)、update_time(数据修改时间)等等。这些字段的填充逻辑大都一样,例如 create_time 一般就是在插入新数据时更新,update_time 则只有在修改数据时更新。之前这些数据的赋值,都是在每个接口中 根据传递的 id 是否存在,单独赋值。这样会造成非常多的冗余代码。
最近我了解到了 Mybatis-Plus 的MetaObjectHandler 机制,可以基于它来完成一些公共字段的赋值。本文就来分享一下,具体的实现细节以及底层原理。
效果演示
我创建了一个 demo 项目来演示效果
首先有一张用户表:
create_time、update_time 就属于我们上面提到的公共字段
项目基于 SpringBoot v2.6.13,引入了 Mybatis-Plus 的 starter、MySQL 驱动、lombok 等依赖,并生成了 t_user 表的增删改查代码,比较简单,就不贴代码了。
创建一个测试 controller,模拟用户表新增和修改数据:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/save")
public String save(@RequestBody UserEntity userEntity) {
userService.save(userEntity);
return "success";
}
@PostMapping("/update")
public String update(@RequestBody UserEntity userEntity) {
userService.updateById(userEntity);
return "success";
}
}
通过 postman 调用保存接口:
结果:
调用修改接口:
结果:
可以看到、新增数据时,自动填充了 create_time、修改数据时,自动填充了 update_time。
实现细节
要实现上面的效果,需要使用到 Mybatis-Plus 中定义的一个接口:MetaObjectHandler,意为元数据处理器,sql 执行时传入的 entity,可以被这个处理器处理,按照自定义的规则填充值。
首先我们要定义一个自定义的处理器,需要实现MetaObjectHandler 接口,重写其中的方法:
@Component
public class CustomMetaObjectHandler implements MetaObjectHandler {
/**
* 插入元对象字段填充(用于插入时对公共字段的填充)
*
* @param metaObject 元对象
*/
@Override
public void insertFill(MetaObject metaObject) {
if (metaObject.hasSetter("createTime")) {
metaObject.setValue("createTime", LocalDateTime.now());
}
}
/**
* 更新元对象字段填充(用于更新时对公共字段的填充)
*
* @param metaObject 元对象
*/
@Override
public void updateFill(MetaObject metaObject) {
if (metaObject.hasSetter("updateTime")) {
metaObject.setValue("updateTime", LocalDateTime.now());
}
}
}
上面代码中可以看出,分别判断了元对象中,是否存在createTime 及updateTime 字段,如果存在,赋值为当前时间。
然后需要在实体类中进行配置:在具体字段的@TableField 注解中配置 fill 属性。
@Data
@TableName("t_user")
public class UserEntity {
/**
* 主键
*/
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
/**
* 姓名
*/
@TableField(value = "name")
private String name;
/**
* 添加时间
*/
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 修改时间
*/
@TableField(value = "update_time", fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
}
这样就可以实现公共字段的自动填充了。
底层原理
前面提到了,我们是使用了 Mybatis-Plus 的MetaObjectHandler 接口,通过重写其中的insertFill 及updateFill 方法,在这两个方法中,完成了公共字段的填充,那我们就主要看一下,在 Mybatis-Plus 的 insert 和 update 操作中,是怎么调用到这两个方法的。
了解过 Mybatis 的可能知道,在 Mybatis 中,所有针对数据库的操作,最终都是调用了 SqlSession 中的方法,而 SqlSession 只有一个默认的实现类DefaultSqlSession、那就从它的源码入手:
在 DefaultSqlsession 中,所有的增删改方法,实际上都是调用了同一个 update 方法,如下:
在这个方法中,具体的操作是委派给了底层的执行器对象,有过 Mybatis 基础的朋友可能知道,在 Mybatis 中,执行器对象的封装应用了装饰器模式,将具体的执行器实例包装到了CachingExecutor 对象中。如下:
再来看CachingExecutor 的 update 方法:
可以看到,在这个类中,将具体的执行器对象实例,包装到了delegate 这个成员变量中,包装的对象为SimpleExecutor,这里继续调用了SimpleExecutor 的 update 方法。
由于SimpleExecutor 继承自一个抽象类BaseExecutor、一些通用的逻辑封装到了BaseExecutor 中,所以这里实际调用的是BaseExecutor 中的 update 方法。
这里又调用了一个doUpdate 方法,这个方法是一个抽象方法
那自然又是调用回了子类SimpleExecutor 中doUpdate 方法的具体实现:
这个方法的源码不难理解,这里创建了一个语句处理器对象,这是 Mybatis 中提供的一个接口,它的主要作用是处理 sql 语句,将封装好的 sql 语句以及执行参数映射为数据库可直接执行的 JDBC 代码。
这个接口类似于执行器对象,也是存在一个装饰类RoutingStatementHandler,以及一个封装了一些公共逻辑的抽象类BaseStatementHandler。如下图:
对于公共参数的赋值,主要代码就在Configuration 全局配置对象的 newStatementHandler 方法中,详细来看一下:
在RoutingStatementHandler 的构造器中,会根据MappedStatement 对象中获取的statementType 的值,创建出不同的实例,并赋值到delegate,在这里,默认创建的是预编译语句处理器对象PreparedStatementHandler。
PreparedStatementHandler 的构造器中,会调用父类的构造方法,也就是前面类图中的抽象类BaseStatementHandler。
在BaseStatementHandler 的构造器中,初始化了一系列的成员变量,其中有一个parameterHandler,它是 Mybatis 中的参数处理器,主要用来处理 sql 的执行参数。
newParameterHandler 方法中,先调用了mappedStatement.getLang()方法,然后调用了createParameterHandler 方法。
下面的代码可以看出,mappedStatement 中 lang 的值为MybatisXMLLanguageDriver、这其实是 Mybatis-Plus 重写了 Mybatis 默认的语言驱动器XMLLanguageDriver,在createParameterHandler 方法时,返回了自定义的MybatisParameterHandler 对象,而不是 Mybatis 默认的DefaultParameterHandler 对象,旨在为 Mybatis-Plus 的各种增强功能提供支持。
MybatisXMLLanguageDriver 中的createParameterHandler 方法,创建了一个MybatisParameterHandler 对象并返回。
MybatisParameterHandler 的构造器中,有一个关键的方法调用:processParameter,意为处理参数。在这个方法中,判断了当前查询方式是否为 INSERT 或 UPDATE,因为这两种方式,有可能需要进行特殊处理。
在process 方法的最后,进行了一个SqlCommandType 的判断,如果为 INSERT ,则调用insertFill,反之,就调用updateFill。
接下来的源码就比较简单了,从全局配置对象 Configuration 中,尝试获取已配置的 MetaObjectHandler 实例,如果存在,就分别调用 insertFill 方法及updateFill 方法。这里实际就会调用我们自定义的MetaObjectHandler 了,这样,就完成了指定公共参数的自动赋值。
总结
以上,就完成了Mybatis-Plus 基于MetaObjectHandler实现公共字段动态赋值以及源码解析。