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

  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 地址: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 容器的拦截器并进行自动注册。如下图,

所以我们只需要定义一个 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】每周分享技术干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!

MyBatis实现动态SQL更新的更多相关文章

  1. MyBatis的动态SQL详解

    MyBatis的动态SQL是基于OGNL表达式的,它可以帮助我们方便的在SQL语句中实现某些逻辑,本文详解mybatis的动态sql,需要的朋友可以参考下 MyBatis 的一个强大的特性之一通常是它 ...

  2. mybatis 使用动态SQL

    RoleMapper.java public interface RoleMapper { public void add(Role role); public void update(Role ro ...

  3. MyBatis框架——动态SQL、缓存机制、逆向工程

    MyBatis框架--动态SQL.缓存机制.逆向工程 一.Dynamic SQL 为什么需要动态SQL?有时候需要根据实际传入的参数来动态的拼接SQL语句.最常用的就是:where和if标签 1.参考 ...

  4. MyBatis探究-----动态SQL详解

    1.if标签 接口中方法:public List<Employee> getEmpsByEmpProperties(Employee employee); XML中:where 1=1必不 ...

  5. mybatis中的.xml文件总结——mybatis的动态sql

    resultMap resultType可以指定pojo将查询结果映射为pojo,但需要pojo的属性名和sql查询的列名一致方可映射成功. 如果sql查询字段名和pojo的属性名不一致,可以通过re ...

  6. MyBatis的动态SQL详解-各种标签使用

    MyBatis的动态SQL是基于OGNL表达式的,它可以帮助我们方便的在SQL语句中实现某些逻辑. MyBatis中用于实现动态SQL的元素主要有: if choose(when,otherwise) ...

  7. 利用MyBatis的动态SQL特性抽象统一SQL查询接口

    1. SQL查询的统一抽象 MyBatis制动动态SQL的构造,利用动态SQL和自定义的参数Bean抽象,可以将绝大部分SQL查询抽象为一个统一接口,查询参数使用一个自定义bean继承Map,使用映射 ...

  8. mybatis 的动态sql语句是基于OGNL表达式的。

    mybatis 的动态sql语句是基于OGNL表达式的.可以方便的在 sql 语句中实现某些逻辑. 总体说来mybatis 动态SQL 语句主要有以下几类:1. if 语句 (简单的条件判断)2. c ...

  9. MyBatis中动态SQL语句完成多条件查询

    一看这标题,我都感觉到是mybatis在动态SQL语句中的多条件查询是多么的强大,不仅让我们用SQL语句完成了对数据库的操作:还通过一些条件选择语句让我们SQL的多条件.动态查询更加容易.简洁.直观. ...

  10. MyBatis中动态SQL元素的使用

    掌握MyBatis中动态SQL元素的使用 if choose(when,otherwise) trim where set foreach <SQL>和<include> 在应 ...

随机推荐

  1. opencv基础

    Python 和 OpenCV 的结合是计算机视觉领域中应用最为广泛的一种方式,它们的结合使得开发者可以快速.高效地完成各种视觉任务.本文将介绍 Python 和 OpenCV 的基础使用,包括安装. ...

  2. Matlab蚁群算法求解旅行商问题

    目录 问题展现 解决代码 代码1 输出结果 代码2 输出结果 代码3 输出结果 问题展现 假设有一个旅行商人要拜访全国 31 个省会城市,他需要选择所要走的路径,路径的限制是每个城市只能拜访一次,而且 ...

  3. 两种路由模式的区别(hash模式,history模式)

    1. hash 带#号的,history不带#号2.hash模式用的hashChange监听路径的变化3.history用的是HTML5相关的API语法 使用pushState => 添加一条历 ...

  4. 简单的了解下 Fetch API 的工作原理

    一.简介 Fetch API是一种现代的Web API,提供了一种异步获取网络资源的方法.由于其简单性.灵活性和一致性,它已经成为Web应用程序中获取数据和资源的流行选择.在本文中,我们将深入探讨Fe ...

  5. 使用Java接入小程序订阅消息!

    更新完微信服务号的模板消息之后,我又赶紧把微信小程序的订阅消息给实现了!之前我一直以为微信小程序也是要企业才能申请,没想到小程序个人就能申请. 消息推送平台推送下发[邮件][短信][微信服务号][微信 ...

  6. k8s介绍与常用命令

    kubernetes基础与常用命令 原文地址 https://blog.csdn.net/footless_bird/article/details/125798691 官方文档 https://ku ...

  7. SqlServer查看表结构

    SELECT CASE WHEN col.colorder = 1 THEN obj.name ELSE '' END AS 表名 ,CASE WHEN col.colorder=1 then isn ...

  8. redission分布式redis锁使用

    public void lock(String key, List<Long> idx) { if (CollectionUtils.isEmpty(idx)) { return; } i ...

  9. 省选联考2021vp记

    卡牌游戏 考虑到将 \(a\) 和 \(b\) 放在一起排序,最后朝上的数字必然在左端点为最小值,右端点为最大值的区间中.这个区间中至少有 \(n-m\) 个是原来的 \(a\),且对于每张卡牌必然要 ...

  10. mysql 新建数据库 排序规则

    utf8_unicode_ci和utf8_general_ci对中.英文来说没有实质的差别.utf8_general_ci校对速度快,但准确度稍差.utf8_unicode_ci准确度高,但校对速度稍 ...