博主记得在一个周五快下班的下午,产品找到我(为什么总感觉周五快下班就来活 ),跟我说有几个业务列表查询需求加上时刻条件过滤数据,这个条件或许会变,不确保以后不修正,这个改动涉及到多个列表查询,所以博主思考了一会想了几种完成方案,

  1. 最简略,直接将时刻条件写死,由 Service 层传递给 Dao 层进行条件拼接。完成上虽然简略,可是代码上感觉非常 low,假如这个参数需求在很多办法里进行传递,那么工作量就比较大。
  2. 杂乱一点,经过 MyBatis 的阻拦器机制,在 SQL 拼接的 prepare 阶段修正 SQL 句子,完成动态 SQL。

考虑到阻拦器机制不需求修正过多代码,因而本文博主将带领我们学习如何运用 MyBatis 阻拦器机制来高雅的完成这个需求。

本文示例代码全部在 Spring Boot3.0、Mybatis Plus3.5.3.1 版别下运转。

简介

MyBatis 是一个流行的 Java 持久层结构,它供给了灵敏的 SQL 映射和履行功用。有时候咱们或许需求在运转时动态地修正 SQL 句子,例如增加一些条件、排序、分页等。MyBatis 供给了一个强大的机制来完成这个需求,那便是阻拦器(Interceptor)。

推荐博主开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包括三个项目:运营后台、H5 商城前台和服务端接口。完成了商城所需的首页展现、产品分类、产品详情、产品 sku、分词查找、购物车、结算下单、付出宝/微信付出、收单谈论以及完善的后台办理等一系列功用。 技能上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块规划、简练易保护,欢迎我们点个 star、关注博主。

github 地址:github.com/wayn111/way…

阻拦器介绍

阻拦器是一种基于 AOP(面向切面编程)的技能,它可以在方针目标的办法履行前后刺进自界说的逻辑。MyBatis 界说了四种类型的阻拦器,分别是:

  • Executor:阻拦履行器的办法,例如 update、query、commit、rollback 等。可以用来完成缓存、业务、分页等功用。
  • ParameterHandler:阻拦参数处理器的办法,例如 setParameters 等。可以用来转化或加密参数等功用。
  • ResultSetHandler:阻拦成果集处理器的办法,例如 handleResultSets、handleOutputParameters 等。可以用来转化或过滤成果集等功用。
  • StatementHandler:阻拦句子处理器的办法,例如 prepare、parameterize、batch、update、query 等。可以用来修正 SQL 句子、增加参数、记载日志等功用。

完成阻拦器

  1. 界说一个完成 org.apache.ibatis.plugin.Interceptor 接口的阻拦器类,并重写其间的 intercept、plugin 和 setProperties 办法。
  2. 增加 @Intercepts 注解,写上需求阻拦的目标和办法,以及办法参数,例如 @Intercepts({@Signature(type = StatementHandler.class, method = “prepare”, args = {Connection.class, Integer.class})}),表明在 SQL 履行之前进行阻拦处理。

注册阻拦器

Spring Boot 项目中集成了 Mybatis Plus 后要让阻拦器收效很简略,Mybatis Plus 的主动装备类会读取项目中所有注册到 Spring 容器的阻拦器并进行主动注册。如下图,

MyBatis实现动态SQL更新

MyBatis实现动态SQL更新

所以咱们只需求界说一个 DynamicSqlInterceptor 阻拦器并加上 @Component 注解就行,代码如下,

@Component
@Slf4j
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class DynamicSqlInterceptor implements Interceptor {
 ...
}

代码示例

yml 装备

指定 xml 文件中需求替换的占位符标识:@dynamicSql 以及待替换日期条件。

# 动态sql装备
dynamicSql:
  placeholder: "@dynamicSql"
  date: "2023-07-10 20:10:30"

Dao 层代码

在需求进行 SQL 占位符替换的办法上加 @DynamicSql 注解。

public interface DynamicSqlMapper {
    @DynamicSql
    Long count();
}

mapper 文件

将日期条件改成占位符 where create_time > @dynamicSql

<mapper namespace="ltd.newbee.mall.core.dao.DynamicSqlMapper">
    <select id="count" resultType="java.lang.Long">
        select count(1) from member
        where create_time > @dynamicSql
    </select>
</mapper>

阻拦器中心代码

@Component
@Slf4j
@Intercepts({
        @Signature(type = StatementHandler.class, 
                method = "prepare", args = {Connection.class, Integer.class})
})
public class DynamicSqlInterceptor implements Interceptor {
    @Value("${dynamicSql.placeholder}")
    private String placeholder;
    @Value("${dynamicSql.date}")
    private  String dynamicDate;
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取 StatementHandler 目标也便是履行句子
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 2. MetaObject 是 MyBatis 供给的一个反射协助类,可以高雅拜访目标的特点,这里是对 statementHandler 目标进行反射处理,
        MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
                        new DefaultReflectorFactory());
        // 3. 经过 metaObject 反射获取 statementHandler 目标的成员变量 mappedStatement
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // mappedStatement 目标的 id 办法返回履行的 mapper 办法的全路径名,如ltd.newbee.mall.core.dao.UserMapper.insertUser
        String id = mappedStatement.getId();
        // 4. 经过 id 获取到 Dao 层类的全限制称号,然后反射获取 Class 目标
        Class<?> classType = Class.forName(id.substring(0, id.lastIndexOf(".")));
        // 5. 获取包括原始 sql 句子的 BoundSql 目标
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        log.info("替换前---sql:{}", sql);
        // 阻拦办法
        String mSql = null;
        // 6. 遍历 Dao 层类的办法
        for (Method method : classType.getMethods()) {
            // 7. 判别办法上是否有 DynamicSql 注解,有的话,就认为需求进行 sql 替换
            if (method.isAnnotationPresent(DynamicSql.class)) {
                mSql = sql.replaceAll(placeholder, String.format("'%s'", dynamicDate));
                break;
            }
        }
        if (StringUtils.isNotBlank(mSql)) {
            log.info("替换后---mSql:{}", mSql);
            // 8. 对 BoundSql 目标经过反射修正 SQL 句子。
            Field field = boundSql.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            field.set(boundSql, mSql);
        }
        // 9. 履行修正后的 SQL 句子。
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        // 运用 Plugin.wrap 办法生成代理目标
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {
        // 获取装备文件中的特点值
    }
}

现在咱们对阻拦器中心代码逻辑进行解说:

  1. 经过 invocation 参数获取 statementHandler 目标,也便是包括拼接后 SQL 句子的目标。
  2. 获取 metaObject 目标, MetaObject 是 MyBatis 供给的一个反射协助类,可以高雅拜访目标的特点,这里是拜访 statementHandler 目标进行反射处理。
  3. 经过 metaObject 反射获取 statementHandler 目标的成员变量 mappedStatement。
  4. 经过 mappedStatement 目标的 id 办法获取到 Dao 层类的全限制称号,然后反射获取 Dao 层类的 Class 目标。
  5. 获取包括原始 SQL 句子的 BoundSql 目标。
  6. 遍历 Dao 层类的办法。
  7. 判别办法上是否有 DynamicSql 注解,有的话就进行时刻条件替换
  8. 对 BoundSql 目标经过反射修正 SQL 句子。
  9. 履行修正后的 SQL 句子。

代码测试

// 测试类
@SpringBootTest
@RunWith(SpringRunner.class)
public class DynamicTest {
    @Autowired
    private DynamicSqlMapper dynamicSqlMapper;
    @Test
    public void test() {
        Long count = dynamicSqlMapper.count();
        Assert.notNull(count, "count不能为null");
    }
}

履行成果:

2023-07-11 22:13:33.375 [main] INFO  l.n.m.config.DynamicSqlInterceptor - [intercept,52] - 替换前---sql:select count(1) from member
        where create_time > @dynamicSql
2023-07-11 22:13:33.376 [main] INFO  l.n.m.config.DynamicSqlInterceptor - [intercept,62] - 替换后---mSql:select count(1) from member
        where create_time > '2023-07-10 20:10:30'

阻拦器应用场景

  • SQL 句子履行监控:可以阻拦履行的 SQL 办法,打印履行的 SQL 句子、参数等信息,而且还可以记载履行的总耗时,可供后期的 SQL 分析时运用。
  • SQL 分页查询:MyBatis 中运用的 RowBounds 运用的内存分页,在分页前会查询所有符合条件的数据,在数据量大的情况下性能较差。经过阻拦器,可以在查询前修正 SQL 句子,提早加上需求的分页参数。
  • 公共字段的赋值:在数据库中通常会有 createTime , updateTime 等公共字段,这类字段可以经过阻拦一致对参数进行的赋值,然后省去手工经过 set 办法赋值的繁琐进程。
  • 数据权限过滤:在很多体系中,不同的用户或许拥有不同的数据拜访权限,例如在多租户的体系中,要做到租户间的数据隔离,每个租户只能拜访到自己的数据,经过阻拦器改写 SQL 句子及参数,可以完成对数据的主动过滤。
  • SQL 句子替换:对 SQL 中条件或者特别字符进行逻辑替换。(也是本文的应用场景)

总结

到此本文解说的 MyBatis 完成动态 SQL 内容就解说结束了,希望我们喜爱。

关注公众号【waynblog】每周共享技能干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!