基于OGNL实现MyBatis-Plus的动态表名拦截

原有动态表插件功能基于拦截器采取的是提取字符串解析表名替换操作,可能会出现表名提取错误操作,不需要替换的表名方法需要加上注解才能忽略。我想设计基于OGNL表达式实现提供一个虚拟表名在运行时动态处理并替换表名。大致实现思路如下:

1. 定义虚拟表名

在 SQL 语句中使用特定格式的虚拟表名占位符。例如,使用 #tableName# 表示虚拟表名。

2. 解析 SQL 语句

在 MyBatis 拦截器中解析 SQL 语句,并检测虚拟表名占位符。

3. 使用 OGNL 表达式计算真实表名

定义一个接口和实现类,用于根据运行时上下文信息动态计算真实表名。

4. 替换虚拟表名

将解析得到的真实表名替换 SQL 语句中的虚拟表名。

5. 集成到拦截器

将上述逻辑集成到 MyBatis 的拦截器中,在 SQL 语句执行前进行处理。

具体实现步骤

1. 定义接口 DynamicTableNameHandler
 public interface DynamicTableNameHandler { 
     String dynamicTableName(Object parameter, String tableName); 
 } 
2. 实现接口 OgnlDynamicTableNameHandler
 import ognl.Ognl;
 import ognl.OgnlException;
 ​
 public class OgnlDynamicTableNameHandler implements DynamicTableNameHandler {
     private String expression;
 ​
     public OgnlDynamicTableNameHandler(String expression) {
         this.expression = expression;
     }
 ​
     @Override
     public String dynamicTableName(Object parameter, String tableName) {
         try {
             Object realTableName = Ognl.getValue(expression, parameter);
             return realTableName != null ? realTableName.toString() : tableName;
         } catch (OgnlException e) {
             throw new RuntimeException("Failed to evaluate OGNL expression", e);
         }
     }
 }
 ​
3. 修改拦截器 DynamicTableNameInnerInterceptor
 import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
 import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
 import com.baomidou.mybatisplus.core.toolkit.TableNameParser;
 import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
 import org.apache.ibatis.executor.Executor;
 import org.apache.ibatis.executor.statement.StatementHandler;
 import org.apache.ibatis.mapping.BoundSql;
 import org.apache.ibatis.mapping.MappedStatement;
 import org.apache.ibatis.mapping.SqlCommandType;
 import org.apache.ibatis.session.ResultHandler;
 import org.apache.ibatis.session.RowBounds;
 import java.sql.Connection;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.List;
 ​
 @Getter
 @Setter
 @NoArgsConstructor
 @SuppressWarnings({"rawtypes"})
 public class DynamicTableNameInnerInterceptor implements InnerInterceptor {
     private DynamicTableNameHandler tableNameHandler;
 ​
     public DynamicTableNameInnerInterceptor(DynamicTableNameHandler tableNameHandler) {
         this.tableNameHandler = tableNameHandler;
     }
 ​
     @Override
     public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
         if (InterceptorIgnoreHelper.willIgnoreDynamicTableName(ms.getId())) return;
         PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
         mpBs.sql(this.changeTable(mpBs.sql(), parameter));
     }
 ​
     @Override
     public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
         PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
         MappedStatement ms = mpSh.mappedStatement();
         SqlCommandType sct = ms.getSqlCommandType();
         if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
             if (InterceptorIgnoreHelper.willIgnoreDynamicTableName(ms.getId())) {
                 return;
             }
             PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
             mpBs.sql(this.changeTable(mpBs.sql(), mpSh.boundSql().getParameterObject()));
         }
     }
 ​
     public String changeTable(String sql, Object parameter) {
         ExceptionUtils.throwMpe(null == tableNameHandler, "Please implement DynamicTableNameHandler processing logic");
         TableNameParser parser = new TableNameParser(sql);
         List<TableNameParser.SqlToken> names = new ArrayList<>();
         parser.accept(names::add);
         StringBuilder builder = new StringBuilder();
         int last = 0;
         for (TableNameParser.SqlToken name : names) {
             int start = name.getStart();
             if (start != last) {
                 builder.append(sql, last, start);
                 builder.append(tableNameHandler.dynamicTableName(parameter, name.getValue()));
             }
             last = name.getEnd();
         }
         if (last != sql.length()) {
             builder.append(sql.substring(last));
         }
         return builder.toString();
     }
 }
 ​
4. 配置拦截器
 DynamicTableNameInnerInterceptor interceptor = new DynamicTableNameInnerInterceptor(
     new OgnlDynamicTableNameHandler("user.role + '_table'")
 );
 ​
 // 将拦截器添加到 MyBatis 配置中
 mybatisConfiguration.addInterceptor(interceptor);
 ​

 更多精彩请关注我的GithubUtopia007 (Qiao Guanhao) · GitHub

<think>好的,我现在需要帮助用户解决在MyBatis Plus中判断传入参数是否为空的最佳实践。首先,我需要回顾用户的问题和之前的对话历史。用户之前询问了如何区分两个List<Map<String, Object>>中的相同值和不同值,我给出了使用HashSet和自定义比较的方法,还提到了处理复杂类型和序列化的方案。现在用户的新问题是关于MyBatis Plus中参数是否为空的判断。 用户提供的引用内容中提到了MyBatis Plus的配置、测试用例、批量删除以及自定义异常接口。其中,引用[1]展示了如何配置MybatisPlusInterceptor,引用[3]是测试用例,引用[4]讨论了批量操作和BatchExecutor,引用[5]涉及自定义异常。这些可能和参数判断有关联吗?比如,在配置拦截器时可能需要处理参数,或者在批量操作中需要检查参数是否为空以避免错误。 用户的需求是判断传入参数是否为空的最佳实践。首先,我需要回忆MyBatis Plus的条件构造器QueryWrapper和UpdateWrapper,它们通常用于构建查询条件,其中可以判断字段是否为空。例如,使用isNull和isNotNull方法。此外,MyBatis Plus的注解@TableField可能也有策略来处理空值,比如在插入或更新时自动忽略空字段。 另外,用户可能在XML映射文件中使用动态SQL标签,如<if>,来根据参数是否为空决定是否包含某段SQL。例如,在update操作中,只更新非空的字段,避免将数据库中的现有值覆盖为null。同时,引用[4]中提到的批量删除和动态SQL拼接可能涉及参数非空判断,以防止SQL语句过大或参数为空导致的错误。 还需要考虑自定义注解或拦截器的可能性。例如,通过自定义拦截器在SQL执行前检查参数,如果必填参数为空则抛出异常。引用[5]中的自定义异常接口BaseErrorInfoInterface可能在这里派上用场,可以定义参数为空的错误码和描述。 同时,全局配置中的jdbcTypeForNull设置也很重要,当字段值为null时,MyBatis会如何处理。默认情况下,可能会转换为OTHER类型,但有些数据库不支持,需要明确指定为NULL。 此外,用户可能希望在不同操作(插入、更新、查询)中有不同的空值处理策略。例如,插入时忽略null字段,更新时同样忽略,而查询时可能需要将空参数作为查询条件的一部分。 需要综合这些方法,给出几种最佳实践,并说明适用场景。例如,使用条件构造器在服务层进行判断,或者在XML中使用动态SQL,或者利用注解配置策略。还要考虑代码的简洁性、维护性和性能影响。 最后,确保回答结构清晰,分步骤说明,并提供代码示例,如使用QueryWrapper的示例、XML中的<if>标签示例,以及全局配置的示例。同时,提醒用户注意jdbcTypeForNull的配置,避免数据库兼容性问题,并建议结合自定义异常处理参数为空的情况,提升系统健壮性。</think>### MyBatis Plus 判断参数为空的最佳实践 在 MyBatis Plus 中判断参数是否为空可以通过以下方案实现,根据不同的场景选择合适的方法: --- #### 1. **使用条件构造器 `QueryWrapper/UpdateWrapper` 的链式方法** 适用于**动态构建查询条件**或**更新操作**时过滤空值。 ```java // 示例:查询 name 不为空且 age 大于 18 的记录 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper .eq(StringUtils.isNotBlank(name), "name", name) // 仅当 name 非空时生效 .gt(age != null, "age", age); List<User> users = userMapper.selectList(queryWrapper); ``` - **`eq(condition, column, value)`**:当 `condition=true` 时添加条件 - **优势**:链式语法简洁,支持空值过滤和复杂逻辑组合[^1] --- #### 2. **XML 映射文件中使用动态 SQL 标签** 适用于**复杂 SQL 逻辑**或需要**复用 SQL 片段**的场景。 ```xml <!-- 示例:根据参数动态更新非空字段 --> <update id="updateUser" parameterType="User"> UPDATE user <set> <if test="name != null and name != ''">name = #{name},</if> <if test="age != null">age = #{age},</if> </set> WHERE id = #{id} </update> ``` - **`<if>` 标签**:通过 OGNL 达式判断参数是否为空 - **注意**:避免 SQL 注入,优先使用 `#{}` 占位符[^2] --- #### 3. **全局配置空值处理策略** 通过 MyBatis Plus 的全局配置,控制**插入/更新操作中空值的处理方式**: ```java @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加空值处理策略(需自定义拦截器或使用已有策略) return interceptor; } } ``` - **字段策略注解**:在实体类字段上使用 `@TableField` ```java public class User { @TableField(insertStrategy = FieldStrategy.NOT_EMPTY) private String name; // 插入时仅当 name 非空才包含该字段 } ``` - **策略选项**: - `FieldStrategy.NOT_NULL`:非 null 时更新 - `FieldStrategy.NOT_EMPTY`:非空(如 String 类型还需非空字符串)[^3] --- #### 4. **自定义注解或拦截器校验参数** 适用于**强制校验必填参数**的业务场景: ```java // 自定义注解 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface RequiredParam { } // AOP 拦截器 @Aspect @Component public class ParamCheckAspect { @Before("@annotation(requiredParam)") public void checkParam(JoinPoint joinPoint, RequiredParam requiredParam) { Object[] args = joinPoint.getArgs(); for (Object arg : args) { if (arg == null || (arg instanceof String && ((String) arg).isEmpty())) { throw new BusinessException("参数不能为空"); // 引用自定义异常接口[^5] } } } } ``` --- #### 5. **处理 JDBC 类型与空值兼容性** 在 `application.yml` 中配置 `jdbcTypeForNull`,避免数据库驱动不兼容: ```yaml mybatis: configuration: jdbc-type-for-null: null # 明确指定 null 值的 JDBC 类型 ``` --- ### 方案对比 | 方法 | 适用场景 | 优点 | 缺点 | |---------------------|----------------------------|--------------------------|-----------------------| | 条件构造器 | 简单查询/更新 | 代码简洁,链式调用 | 复杂 SQL 支持有限 | | XML 动态 SQL | 复杂业务逻辑 | 灵活,支持复用 SQL 片段 | 需维护 XML 文件 | | 全局字段策略 | 统一控制空值行为 | 配置简单,减少重复代码 | 无法覆盖所有特殊场景 | | 自定义校验 | 强制参数校验 | 业务逻辑与数据层解耦 | 增加代码复杂度 | --- ### 推荐实践 1. **优先使用条件构造器**处理简单空值逻辑(如查询过滤) 2. **复杂业务逻辑**使用 XML 动态 SQL 标签(如多关联更新) 3. **关键参数**通过自定义注解+AOP拦截器强制校验(如支付接口的金额字段)[^5] 4. 全局配置 `jdbcTypeForNull` 避免数据库兼容性问题[^4] --- ### 示例代码 ```java // 使用条件构造器实现动态更新 public void updateUserIfNotNull(User user) { UpdateWrapper<User> updateWrapper = new UpdateWrapper<>(); updateWrapper .set(user.getName() != null, "name", user.getName()) .set(user.getAge() != null, "age", user.getAge()) .eq("id", user.getId()); userMapper.update(null, updateWrapper); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值