Mybatis-Plus 自动属性填充与自定义Insert into语句顺序&MyBatisPlus中使用 @TableField完成字段自动填充

前言:系统中使用了Mybatis-Plus 自动属性填充为实体统一进行属性的填值,在Mapper的xml 文件中 insert into 语句 使用
id, 进行判断会发现该属性是空的,明明已经为改字段进行了属性的自动填充,为什么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 test="testFiled != null">#{testFiled },
  </trim>

在对实体id 设置完成之后,进行数据的插入,发现插入的数据中只有id 没有testFiled 属性;

2 推断问题产生的原因:

原因1:属性填充正常,但是在xml sql 语句中,在某些情况下 判断有问题;

原因2:自定义填充属性有问题,导致想要填充的属性没有被填充值,导致进行 判断有问题;

愿意3:属性填充和 标签判断都没有问题,但是sql 拼接的时机 在属性填充之前进行;

标签只进行简单的空判断,出问题的可能性不大,从原因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的拼接,此时进行 判断时 发现改属性为空,必然会跳过了该属性的拼接,即使后面自动填充为属性填充了数据,但是由于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},
     #{testFiled },
  </trim>

此时在次进行插入,发现插入成功,并且testFiled 属性也是有值的;

3 从Mybatis-Plus 代码层面查看sql 语句的拼接:

3.1 先看下sql 拼接的流程:
MybatisParameterHandler 是 Mybatis 中用于处理数据库操作参数的接口,它的实现类 DefaultParameterHandler 负责将 Java 对象转换为 JDBC 预处理语句需要的参数值,以及将参数值设置到预处理语句中。其中,BoundSql 对象就是用于封装 SQL 语句和对应的参数值的。

BoundSql 对象的赋值过程主要由 SqlSource 和 ParameterMapping 来完成,具体流程如下:

  1. Mybatis 在执行 SQL 语句之前,会根据 Mapper 接口定义的方法和传入的参数生成 MappedStatement 对象。在 MappedStatement 中包含了 SQL 语句、参数映射信息等相关的元数据。
  2. MappedStatement 负责生成 BoundSql 对象。在生成 BoundSql 对象时,Mybatis 会首先根据 SQL 语句和参数信息生成一个 StaticSqlSource 对象,然后再通过它生成一个 DynamicSqlSource 对象。DynamicSqlSource 会根据传入的参数信息和 Mapper 接口定义的 SQL 语句,动态生成最终的 SQL 语句和参数值。这个过程中,ParameterMapping 负责将 Java 对象中的属性值和 SQL 语句中的占位符做映射关联,SqlSource 负责根据参数信息和 SQL 语句生成 BoundSql 对象。
  3. 生成 BoundSql 对象后,Mybatis 会通过 ParameterHandler 将 BoundSql 对象中的 SQL 语句和参数值设置到 JDBC 预处理语句中。默认的 ParameterHandler 实现类是 DefaultParameterHandler,它会通过反射获取 PreparedStatement 对象,并调用 setXxx() 方法将参数值设置到预处理语句中。在设置参数值的过程中,DefaultParameterHandler 会根据 ParameterMapping 中的信息获得 Java 对象中对应属性的值,并将其赋值给 BoundSql 对象中对应的参数占位符。
  4. 综上所述,BoundSql 对象的赋值过程主要由 SqlSource 和 ParameterMapping 来完成。它们会根据传入的参数信息和 Mapper 接口定义的 SQL 语句,动态生成最终的 SQL 语句和参数值,并将它们设置到 BoundSql 对象中。

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;
    }
}

可以看到这里会根据标签不同调用不同的实现完成判断:
Mybatis-Plus 自动属性填充与自定义Insert into语句顺序&MyBatisPlus中使用 @TableField完成字段自动填充_第1张图片
并且会逐个进行属性的判断,只有为true 才进行属性拼接:
Mybatis-Plus 自动属性填充与自定义Insert into语句顺序&MyBatisPlus中使用 @TableField完成字段自动填充_第2张图片
以IfSqlNode 为例,可以看出只有当属性不为空时,才返回true 否则返回false,只有在返回true 时后续才会对改属性进行拼接
Mybatis-Plus 自动属性填充与自定义Insert into语句顺序&MyBatisPlus中使用 @TableField完成字段自动填充_第3张图片

4 总结:

Mybatis-Plus 自定义的sql 语句其BoundSql的解析和拼接是在属性填充之前进行的,所以如果在自定义sql 语句中使用了标签进行属性的非空判断,就不会拼接改属性,此时需要在自定义的sql 中去除<的非空判断直接使用#{testFiled },这样最终在进数据插入时,Mybatis会动态的替换掉改占位符。



MyBatisPlus中使用 @TableField完成字段自动填充

引言

MyBatisPlus是一款强大的Java持久层框架,它在MyBatis的基础上进行了功能扩展和优化。其中,自动填充字段是一个常见的需求,可以通过使用MyBatisPlus的注解@TableField来实现。本文将介绍如何使用@TableField注解完成字段自动填充的功能。

@TableField注解简介

@TableField注解是MyBatisPlus提供的用于实体类字段的注解,用于配置字段的属性和行为。其中,我们可以通过设置fill属性来实现字段自动填充的功能。

使用方法

下面是使用@TableField注解完成字段自动填充的步骤:

1. 在实体类中添加字段

首先,在实体类中添加需要自动填充的字段。例如,我们在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方法
}
2. 配置字段填充处理器

接下来,我们需要配置字段填充的处理器。在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两个方法,分别用于在插入和更新操作时自动填充字段的值。

3. 配置MyBatisPlus的自动填充

最后,我们需要在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中,并将其作为一个内部拦截器。

4. 测试自动填充功能

现在,我们可以进行测试,看看自动填充功能是否生效。

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进行开发时有所帮助!

你可能感兴趣的:(Java细节,Java,Mybatis-Plus,TableField,填充)