赞
踩
前言:系统中使用了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进行开发时有所帮助!
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。