前言:系统中使用了Mybatis-Plus 自动属性填充为实体统一进行属性的填值,在Mapper的xml 文件中 insert into 语句 使用
<if test="id != null">id,</if>
进行判断会发现该属性是空的,明明已经为改字段进行了属性的自动填充,为什么Mybatis- 在拼接sql 语句时依然认为 改属性是空的呢;
1 问题重现:
1.1 在实体中使用了属性填充属性:
@TableField(fill = FieldFill.INSERT)
private String testFiled;
1.2 在拦截器里进行了属性填充:
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("testFiled", "test", metaObject);
}
1.3 mapper xml :
insert into ${prefix}knowledge_authority
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="testFiled != null">test_filed,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="testFiled != null">#{testFiled },</if>
</trim>
在对实体id 设置完成之后,进行数据的插入,发现插入的数据中只有id 没有testFiled 属性;
2 推断问题产生的原因:
原因1:属性填充正常,但是在xml sql 语句中,在某些情况下 <if>
判断有问题;
原因2:自定义填充属性有问题,导致想要填充的属性没有被填充值,导致进行 <if>
判断有问题;
愿意3:属性填充和 <if>
标签判断都没有问题,但是sql 拼接的时机 在属性填充之前进行;
<if>
标签只进行简单的空判断,出问题的可能性不大,从原因2 入手:
使用自定义属性填充时,会调用MybatisParameterHandler 类中的process()方法完成属性填充的调用;
private void process(Object parameter) {
if (parameter != null) {
TableInfo tableInfo = null;
Object entity = parameter;
if (parameter instanceof Map) {
Map<?, ?> map = (Map)parameter;
if (map.containsKey("et")) {
Object et = map.get("et");
if (et != null) {
entity = et;
tableInfo = TableInfoHelper.getTableInfo(et.getClass());
}
}
} else {
tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
}
if (tableInfo != null) {
MetaObject metaObject = this.configuration.newMetaObject(entity);
if (SqlCommandType.INSERT == this.sqlCommandType) {
// 插入时 id 的填充
this.populateKeys(tableInfo, metaObject, entity);
// 这里会在insert 时 调用我们自己定义的拦截器进行属性的自动填充
this.insertFill(metaObject, tableInfo);
} else {
// 这里会在update 时 调用我们自己定义的拦截器进行属性的自动填充
this.updateFill(metaObject, tableInfo);
}
}
}
}
通过debug 我们发现,在插入数据时确实调用了process 方法,并对实体完成了属性的填充,属性填充是正常的;所以会不会是原因3 ,属性填充的时机和sql 拼接的时机不同造成的。
如果 先进行了sql的拼接,此时进行 <if>
判断时 发现改属性为空,必然会跳过了该属性的拼接,即使后面自动填充为属性填充了数据,但是由于sql已经完成了拼接,最终执行的sql 也是没有该属性的;
基于此猜想,我们将xml 中 判断标签去掉,只保留占位符:
insert into ${prefix}knowledge_authority
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
test_filed,
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
#{testFiled },
</trim>
此时在次进行插入,发现插入成功,并且testFiled 属性也是有值的;
3 从Mybatis-Plus 代码层面查看sql 语句的拼接:
3.1 先看下sql 拼接的流程:
MybatisParameterHandler 是 Mybatis 中用于处理数据库操作参数的接口,它的实现类 DefaultParameterHandler 负责将 Java 对象转换为 JDBC 预处理语句需要的参数值,以及将参数值设置到预处理语句中。其中,BoundSql 对象就是用于封装 SQL 语句和对应的参数值的。
BoundSql 对象的赋值过程主要由 SqlSource 和 ParameterMapping 来完成,具体流程如下:
3.2 MybatisParameterHandler 中的 BoundSql boundSql:
public class MybatisParameterHandler implements ParameterHandler {
private final TypeHandlerRegistry typeHandlerRegistry;
private final MappedStatement mappedStatement;
private final Object parameterObject;
private final BoundSql boundSql;
private final Configuration configuration;
private final SqlCommandType sqlCommandType;
public MybatisParameterHandler(MappedStatement mappedStatement, Object parameter, BoundSql boundSql) {
this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
this.mappedStatement = mappedStatement;
// 拼接好的sql
this.boundSql = boundSql;
this.configuration = mappedStatement.getConfiguration();
this.sqlCommandType = mappedStatement.getSqlCommandType();
// 主键id 和 属性的自动填充
this.parameterObject = this.processParameter(parameter);
}
}
可以看到在创建MybatisParameterHandler 对象时,boundSql 已经完成了sql 的解析和拼接,然后在this.processParameter(parameter) 方法完成了主键id 和 属性的自动填充,从构造方法可以看到,boundSql 的拼接是先于processParameter(parameter) 属性填充的方法的,这就解释了为什么我们明明已经为改属性进行了填充,为什么 最终自定义的insert into 语句 标签判断是空的,本质就是因为两者的顺序问题;
3.3 sql 语句的拼接:
进入DynamicSqlSource 类getBoundSql 方法:
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
public BoundSql getBoundSql(Object parameterObject) {
// 参数解析
DynamicContext context = new DynamicContext(this.configuration, parameterObject);
// sql 拼接
this.rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(this.configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 占位符拼接
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// sql 拼接
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
this.rootSqlNode.apply(context):
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
public boolean apply(DynamicContext context) {
// 这里会判断xml 中的所有属性标签,只有判断为true ,才进行属性的拼接
this.contents.forEach((node) -> {
node.apply(context);
});
return true;
}
}
可以看到这里会根据标签不同调用不同的实现完成判断:
并且会逐个进行属性的判断,只有为true 才进行属性拼接:
以IfSqlNode 为例,可以看出只有当属性不为空时,才返回true 否则返回false,只有在返回true 时后续才会对改属性进行拼接
4 总结:
Mybatis-Plus 自定义的sql 语句其BoundSql的解析和拼接是在属性填充之前进行的,所以如果在自定义sql 语句中使用了<if>
标签进行属性的非空判断,就不会拼接改属性,此时需要在自定义的sql 中去除<<if>
的非空判断直接使用#{testFiled },这样最终在进数据插入时,Mybatis会动态的替换掉改占位符。
MyBatisPlus是一款强大的Java持久层框架,它在MyBatis的基础上进行了功能扩展和优化。其中,自动填充字段是一个常见的需求,可以通过使用MyBatisPlus的注解@TableField来实现。本文将介绍如何使用@TableField注解完成字段自动填充的功能。
@TableField注解是MyBatisPlus提供的用于实体类字段的注解,用于配置字段的属性和行为。其中,我们可以通过设置fill属性来实现字段自动填充的功能。
下面是使用@TableField注解完成字段自动填充的步骤:
首先,在实体类中添加需要自动填充的字段。例如,我们在User实体类中添加createTime和updateTime字段,用于记录创建时间和更新时间。
javaCopy codepublic class User {
private Long id;
private String name;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
// 省略getter和setter方法
}
接下来,我们需要配置字段填充的处理器。在MyBatisPlus中,我们可以通过实现MetaObjectHandler接口来自定义填充处理器。
javaCopy code@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
}
在上述代码中,我们实现了insertFill和updateFill两个方法,分别用于在插入和更新操作时自动填充字段的值。
最后,我们需要在MyBatisPlus的配置文件中进行配置,以启用自动填充功能。
@Configuration
public class MyBatisPlusConfig {
@Autowired
private MyMetaObjectHandler myMetaObjectHandler;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new MetaObjectHandlerInterceptor(myMetaObjectHandler));
return interceptor;
}
在上述代码中,我们将自定义的填充处理器MyMetaObjectHandler添加到MybatisPlusInterceptor中,并将其作为一个内部拦截器。
现在,我们可以进行测试,看看自动填充功能是否生效。
javaCopy codeUser user = new User();
user.setName("John");
userService.save(user);
在上述代码中,我们创建了一个User对象,并设置了name属性的值为"John"。当调用userService的save方法保存对象时,createTime和updateTime字段将会被自动填充为当前的时间。
以下是一个示例代码,演示了如何使用@TableField注解完成字段自动填充的功能:
// User.java
public class User {
private Long id;
private String name;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
// 省略getter和setter方法
}
// MyMetaObjectHandler.java
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
}
// MyBatisPlusConfig.java
@Configuration
public class MyBatisPlusConfig {
@Autowired
private MyMetaObjectHandler myMetaObjectHandler;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new MetaObjectHandlerInterceptor(myMetaObjectHandler));
return interceptor;
}
}
// UserService.java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void save(User user) {
userMapper.insert(user);
}
}
// UserController.java
@RestController
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/users")
public void createUser(@RequestBody User user) {
userService.save(user);
}
}
在上述示例代码中,我们定义了一个User实体类,其中包含了需要自动填充的createTime和updateTime字段。我们使用@TableField注解来标记这两个字段,通过设置fill属性为FieldFill.INSERT和FieldFill.INSERT_UPDATE来指定字段自动填充的时机。 我们还定义了一个MyMetaObjectHandler类,实现了MetaObjectHandler接口,并重写了insertFill和updateFill方法来实现具体的字段填充逻辑。 在MyBatisPlusConfig类中,我们将自定义的填充处理器MyMetaObjectHandler添加到MybatisPlusInterceptor中,并作为一个内部拦截器。 最后,在UserController中,我们通过调用userService的save方法来保存用户对象。当保存操作触发时,createTime和updateTime字段将会被自动填充为当前的时间。 请根据实际需求,修改代码中的参数和逻辑以适应你的项目。
MetaObjectHandlerInterceptor 类是一个自定义的实现类,用于实现 MyBatis-Plus 框架的拦截器功能。下面是一个可能的 MetaObjectHandlerInterceptor 类的代码示例:
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import java.util.Properties;
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class MetaObjectHandlerInterceptor implements Interceptor {
private MyMetaObjectHandler myMetaObjectHandler; // 自定义的 MetaObjectHandler
public MetaObjectHandlerInterceptor(MyMetaObjectHandler myMetaObjectHandler) {
this.myMetaObjectHandler = myMetaObjectHandler;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
// 在执行 update 方法之前,调用自定义的 MetaObjectHandler 填充数据
myMetaObjectHandler.insertFill(mappedStatement, parameter);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 设置拦截器的属性
}
}
上述代码使用了 MyBatis 的拦截器功能 (@Intercepts 和 @Signature 注解) 来实现对 Executor 类的 update 方法进行拦截。在拦截方法中,将会调用自定义的 MetaObjectHandler 的 insertFill 方法进行数据填充,然后再继续执行原始的方法逻辑。 请注意,上述代码中的 MyMetaObjectHandler
是自定义的 MyMetaObjectHandler
类,需要根据自己的业务逻辑来实现该类。MyMetaObjectHandler
可以继承 com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
类,并重写对应的方法,以实现数据库操作前后的自定义处理逻辑。
一个实际的应用场景是,在用户注册时,自动为用户生成一个唯一的邀请码,并将邀请码存储到用户表中。以下是一个示例代码:
javaCopy code// User.java
public class User {
private Long id;
private String name;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
@TableField(fill = FieldFill.INSERT)
private String inviteCode;
// 省略getter和setter方法
}
// MyMetaObjectHandler.java
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
this.strictInsertFill(metaObject, "inviteCode", String.class, generateInviteCode());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
private String generateInviteCode() {
// 生成唯一的邀请码逻辑
// 可以使用UUID、随机字符串等方式生成唯一的邀请码
// 这里只是一个示例,实际应用中需要根据具体需求进行处理
return UUID.randomUUID().toString().replace("-", "");
}
}
// MyBatisPlusConfig.java
@Configuration
public class MyBatisPlusConfig {
@Autowired
private MyMetaObjectHandler myMetaObjectHandler;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new MetaObjectHandlerInterceptor(myMetaObjectHandler));
return interceptor;
}
}
// UserService.java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void save(User user) {
userMapper.insert(user);
}
}
// UserController.java
@RestController
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/users")
public void createUser(@RequestBody User user) {
userService.save(user);
}
}
在上述示例代码中,我们新增了一个inviteCode字段,并使用@TableField注解将其标记为需要自动填充的字段。在MyMetaObjectHandler类的insertFill方法中,我们通过调用generateInviteCode方法生成一个唯一的邀请码,并将其填充到inviteCode字段中。generateInviteCode方法中使用UUID来生成一个唯一的字符串作为邀请码,然后去掉其中的"-"字符。 当用户注册时,会调用UserController中的createUser方法,该方法会调用userService的save方法保存用户对象。在保存操作触发时,createTime、updateTime和inviteCode字段将会被自动填充。inviteCode字段的值将会是一个唯一的邀请码。 请根据你的实际需求,修改代码中的参数和逻辑以适应你的项目。
通过使用@TableField注解和自定义的MetaObjectHandler填充处理器,我们可以很方便地实现字段的自动填充功能。在MyBatisPlus中,这个功能可以简化我们的开发工作,提高代码的可维护性和可读性。希望本文对大家在使用MyBatisPlus进行开发时有所帮助!