MyBatis实现动态SQL更新
博主记得在一个周五快下班的下午,产品找到我(为什么总感觉周五快下班就来活 ),跟我说有几个业务列表查询需要加上时间条件过滤数据,这个条件可能会变,不保证以后不修改,这个改动涉及到多个列表查询,于是博主思考了一会想了几种实现方案,
- 最简单,直接将时间条件写死,由 Service 层传递给 Dao 层进行条件拼接。实现上虽然简单,但是代码上感觉非常 low,如果这个参数需要在很多方法里进行传递,那么工作量就比较大。
- 复杂一点,通过 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 语句、添加参数、记录日志等功能。
实现拦截器
- 定义一个实现 org.apache.ibatis.plugin.Interceptor 接口的拦截器类,并重写其中的 intercept、plugin 和 setProperties 方法。
- 添加 @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) {
// 获取配置文件中的属性值
}
}
现在我们对拦截器核心代码逻辑进行讲解:
- 通过 invocation 参数获取 statementHandler 对象,也就是包含拼接后 SQL 语句的对象。
- 获取 metaObject 对象, MetaObject 是 MyBatis 提供的一个反射帮助类,可以优雅访问对象的属性,这里是访问 statementHandler 对象进行反射处理。
- 通过 metaObject 反射获取 statementHandler 对象的成员变量 mappedStatement。
- 通过 mappedStatement 对象的 id 方法获取到 Dao 层类的全限定名称,然后反射获取 Dao 层类的 Class 对象。
- 获取包含原始 SQL 语句的 BoundSql 对象。
- 遍历 Dao 层类的方法。
- 判断方法上是否有 DynamicSql 注解,有的话就进行时间条件替换。
- 对 BoundSql 对象通过反射修改 SQL 语句。
- 执行修改后的 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更新的更多相关文章
- MyBatis的动态SQL详解
MyBatis的动态SQL是基于OGNL表达式的,它可以帮助我们方便的在SQL语句中实现某些逻辑,本文详解mybatis的动态sql,需要的朋友可以参考下 MyBatis 的一个强大的特性之一通常是它 ...
- mybatis 使用动态SQL
RoleMapper.java public interface RoleMapper { public void add(Role role); public void update(Role ro ...
- MyBatis框架——动态SQL、缓存机制、逆向工程
MyBatis框架--动态SQL.缓存机制.逆向工程 一.Dynamic SQL 为什么需要动态SQL?有时候需要根据实际传入的参数来动态的拼接SQL语句.最常用的就是:where和if标签 1.参考 ...
- MyBatis探究-----动态SQL详解
1.if标签 接口中方法:public List<Employee> getEmpsByEmpProperties(Employee employee); XML中:where 1=1必不 ...
- mybatis中的.xml文件总结——mybatis的动态sql
resultMap resultType可以指定pojo将查询结果映射为pojo,但需要pojo的属性名和sql查询的列名一致方可映射成功. 如果sql查询字段名和pojo的属性名不一致,可以通过re ...
- MyBatis的动态SQL详解-各种标签使用
MyBatis的动态SQL是基于OGNL表达式的,它可以帮助我们方便的在SQL语句中实现某些逻辑. MyBatis中用于实现动态SQL的元素主要有: if choose(when,otherwise) ...
- 利用MyBatis的动态SQL特性抽象统一SQL查询接口
1. SQL查询的统一抽象 MyBatis制动动态SQL的构造,利用动态SQL和自定义的参数Bean抽象,可以将绝大部分SQL查询抽象为一个统一接口,查询参数使用一个自定义bean继承Map,使用映射 ...
- mybatis 的动态sql语句是基于OGNL表达式的。
mybatis 的动态sql语句是基于OGNL表达式的.可以方便的在 sql 语句中实现某些逻辑. 总体说来mybatis 动态SQL 语句主要有以下几类:1. if 语句 (简单的条件判断)2. c ...
- MyBatis中动态SQL语句完成多条件查询
一看这标题,我都感觉到是mybatis在动态SQL语句中的多条件查询是多么的强大,不仅让我们用SQL语句完成了对数据库的操作:还通过一些条件选择语句让我们SQL的多条件.动态查询更加容易.简洁.直观. ...
- MyBatis中动态SQL元素的使用
掌握MyBatis中动态SQL元素的使用 if choose(when,otherwise) trim where set foreach <SQL>和<include> 在应 ...
随机推荐
- day33:进程锁&事件&进程队列&进程间共享数据
目录 1.锁:Lock 2.信号量:Semaphone 3.事件:Event 4.进程队列:Queue 5.生产者和消费者模型 6.JoinableQueue 7.Manager:进程之间共享数据 锁 ...
- 在smt贴片加工中手工焊接和机器焊接的区别
在smt贴片加工领域,都需要将电子元件贴装在pcb板表面并进行焊接的,常用的焊接方式分为两种:手动焊接和全自动机器焊接,而常用的焊接机器有回流焊机和波峰焊机,那你知道他们的区别是什么吗?安徽英特丽带你 ...
- ubuntu搭建深度学习环境
安装vscode 方法一: 依次输入如下命令 1.sudo add-apt-repository ppa:ubuntu-desktop/ubuntu-make 2.sudo apt-get updat ...
- 龙芯(Loongarch64),在Linux虚拟一个龙芯OS体验下
前言 想体验下龙芯OS,但是又没有龙芯开发板或者龙芯实体机.手头上只有一个X64环境的Linux发行版,应该怎么做呢? 概括 其实非常简单,可以通过Chroot命令和Qemu在X64的指令集系统上模拟 ...
- P4055 [JSOI2009] 游戏(博弈论 、最大匹配)
P4055 [JSOI2009] 游戏 目录 P4055 [JSOI2009] 游戏 题目传送门 题目大意 : 思路: code 题目传送门 题目大意 : 小AA和小YY玩游戏,在这个游戏中,同一个格 ...
- HTML+CSS仿写的登录页面
仿写的登录页面 使用HTML+CSS,感觉很简单,记录下 话不多说,直接上代码 <!DOCTYPE html> <html lang="en"> & ...
- 海思码率控制相关参数调优(CBR/VBR)
1.CBR 海思相关参数调整(在Hisi板,cat /proc/umap/rc 可查看相关参数变化) 1.1 RC参数 1.2 VENC参数 VENC_PARAM_H264_CBR_S/VENC_PA ...
- Windows下cygwin编译redis源码
准备环境 安装cygwin64 下载地址:https://www.cygwin.com/运行安装程序,一直下一步选择阿里镜像,目前试过163,会比阿里慢需要选择的包有make.pkg-config.p ...
- 2022-10-20:以下go语言代码输出什么?A:7;B:7.0;C:0;D:编译错误。 package main import ( “fmt“ ) func main() { const
2022-10-20:以下go语言代码输出什么?A:7:B:7.0:C:0:D:编译错误. package main import ( "fmt" ) func main() { ...
- 2022-10-15:给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。 你可以按 任意顺序 返回答案。 要求时间复杂度O(N)。 输入: nums = [1,1,1
2022-10-15:给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素. 你可以按 任意顺序 返回答案. 要求时间复杂度O(N). 输入: nums = [1,1,1 ...