您当前的位置:首页 > 电脑百科 > 程序开发 > 框架

MyBatis实现SQL占位符替换

时间:2023-07-12 13:35:55  来源:微信公众号  作者:waynaqua

博主记得在一个周五快下班的下午,产品找到我,跟我说有几个业务列表查询需要加上时间条件过滤数据,这个条件可能会变,不保证以后不修改,这个改动涉及到多个列表查询,于是博主思考了一会想了几种实现方案,

  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,整合了 MySQLredis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。

Github 地址:https://github.com/wayn111/waynboot-mall

拦截器介绍

拦截器是一种基于 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 容器的拦截器并进行自动注册。如下图,MybatisPlusAutoConfiguration

图片

图片
注册拦截器

所以我们只需要定义一个 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 内容就讲解完毕了,希望大家喜欢。



Tags:MyBatis   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除。
▌相关推荐
我无法明确的告诉你JPA和MyBatis在国内哪个会更流行,我本人更喜欢JPA,但是我本人日常开发用MyBatis多。但是我的回答绝对不是在划水,而是我多年来自己的一点小小的思考。MyBati...【详细内容】
2023-08-22  Tags: MyBatis  点击:(9)  评论:(0)  加入收藏
一、场景还原1.版本信息MySQL版本:5.6.36-82.1-logMybatis-Plus的starter版本:3.3.2存储引擎:InnoDB2.死锁现象A同学在生产环境使用了Mybatis-Plus提供的com.baomidou.mybatisp...【详细内容】
2023-08-14  Tags: MyBatis  点击:(6)  评论:(0)  加入收藏
我无法明确的告诉你JPA和MyBatis在国内哪个会更流行,我本人更喜欢JPA,但是我本人日常开发用MyBatis多。但是我的回答绝对不是在划水,而是我多年来自己的一点小小的思考。MyBati...【详细内容】
2023-07-29  Tags: MyBatis  点击:(33)  评论:(0)  加入收藏
最近逛开源社区的时候,偶尔发现了一个 MyBatis 增强框架-MyBatis-Flex ,感觉挺不错的,集合了 MyBatis-Plus、Fluent-MyBatis 的优点。 MyBatis-Flex 的作者是一位技术大佬,还开...【详细内容】
2023-07-27  Tags: MyBatis  点击:(39)  评论:(0)  加入收藏
博主记得在一个周五快下班的下午,产品找到我(为什么总感觉周五快下班就来活...【详细内容】
2023-07-12  Tags: MyBatis  点击:(0)  评论:(0)  加入收藏
MyBatis-Plus则是一个优秀的ORM框架,它为我们封装了大量的数据库操作细节,简化了我们的开发工作,同时也提供了多数据源方案。一、前言随着业务的不断扩展和复杂度的增加,我们在...【详细内容】
2023-06-07  Tags: MyBatis  点击:(91)  评论:(0)  加入收藏
MyBatis是一款优秀的开源持久层框架,支持自定义SQL查询、存储过程和高级映射,相信很多小伙伴都在使用。在MyBatis中,我们可以在XML中编写SQL语句,然后绑定到Java方法中,通过参数...【详细内容】
2023-06-06  Tags: MyBatis  点击:(7339)  评论:(0)  加入收藏
用来循环容器的标签forEach,查看例子foreach元素的属性主要有item,index,collection,open,separator,close。 item:集合中元素迭代时的别名, index:集合中元素迭代时的索引 open:常用...【详细内容】
2023-05-22  Tags: MyBatis  点击:(150)  评论:(0)  加入收藏
MyBatis 是一个优秀的持久层框架,它提供了丰富的 SQL 映射功能,可以让我们通过 XML 或注解方式来定义 SQL 语句。它很大程度上简化了数据库操作,提高了开发效率。动态 SQL 是...【详细内容】
2023-05-12  Tags: MyBatis  点击:(166)  评论:(0)  加入收藏
今天了不起来说说这个关于 Mybatis ,为什么要说 Mybatis 呢?因为现在面试的时候,除了那些最基础的,比如如何防止 SQL 注入,以及 Mybatis 的一级缓存,二级缓存之后,还有一些其他的...【详细内容】
2023-04-28  Tags: MyBatis  点击:(171)  评论:(0)  加入收藏
▌简易百科推荐
Java集合框架是Java编程中不可或缺的一部分,它提供了一组用于存储和操作数据的类和接口。在使用集合框架时,选择正确的数据结构非常重要,可以显著提升程序的性能和效率。下面将...【详细内容】
2023-09-05  编程技术汇  今日头条  Tags:框架   点击:(5)  评论:(0)  加入收藏
作者:MMLab@NTU来自南洋理工大学和商汤科技的研究者联合提出了一种全新的文本驱动动作生成框架&mdash;&mdash;ReMoDiffuse。人体动作生成任务旨在生成逼真的人体动作序列,以满...【详细内容】
2023-09-05    机器之心  Tags:ReMoDiffuse   点击:(6)  评论:(0)  加入收藏
南开大学的李世阳、彭钰婷以《基于oneAPI大规模图计算异构加速框架设计》为题,分享了基于oneAPI设计的oneGRAPH在图计算领域带来的性能提升。以下内容根据李世阳、彭钰婷演讲...【详细内容】
2023-08-31  DT Value    Tags:oneAPI   点击:(5)  评论:(0)  加入收藏
深度学习已成为计算机科学领域中的热门技术,为人工智能带来了重大突破。在深度学习的发展过程中,各种深度学习框架相继涌现,其中以TensorFlow为代表的框架受到了广泛关注和应用...【详细内容】
2023-08-31  亚托克索的日记    Tags:框架   点击:(5)  评论:(0)  加入收藏
亲爱的 Javaer 们,在平时编码的过程中,你是否曾想过编写一个 Java 框架去为开发提效?但是要么编写框架时感觉无从下手,不知道从哪开始。要么有思路了后对某个功能实现的技术细节...【详细内容】
2023-08-25    京东云开发者  Tags:框架   点击:(12)  评论:(0)  加入收藏
欢迎来到写代码那些事 !本教程将带您逐步深入了解使用 PyQt5 创建丰富、交互性强的图形用户界面(GUI)应用程序。无论您是新手还是有经验的开发者,通过本教程,您将学会如何利用 Py...【详细内容】
2023-08-24  写代码那些事    Tags:框架   点击:(3)  评论:(0)  加入收藏
环境:springboot2.5.12经常会遇到在项目中调用第三方接口的情景,你是如何调用的呢?同步?异步?场景:假设下单业务流程如下步骤:1、查询用户信息。2、查询库存信息。3、查询活动信息...【详细内容】
2023-08-23   Spring全家桶实战案例源码    Tags:SpringBoot   点击:(10)  评论:(0)  加入收藏
Spring Boot与微服务微服务架构是当下构建互联网应用的主流架构。在Spring家族中,专门有着用于构建微服务架构的Spring Cloud框架。而Spring Cloud框架本身则是构建在Spring...【详细内容】
2023-08-23  程序员高级码农II  今日头条  Tags:SpringBoot   点击:(4)  评论:(0)  加入收藏
我无法明确的告诉你JPA和MyBatis在国内哪个会更流行,我本人更喜欢JPA,但是我本人日常开发用MyBatis多。但是我的回答绝对不是在划水,而是我多年来自己的一点小小的思考。MyBati...【详细内容】
2023-08-22    老诚不bug  Tags:Spring   点击:(9)  评论:(0)  加入收藏
近年来,基于扩散模型(Diffusion Models)的图像生成模型层出不穷,展现出令人惊艳的生成效果。然而,现有相关研究模型代码框架存在过度碎片化的问题,缺乏统一的框架体系,导致出现「迁...【详细内容】
2023-08-22    机器之心Pro  Tags:Diffusion模型   点击:(7)  评论:(0)  加入收藏
站内最新
站内热门
站内头条