博主记得在一个周五快下班的下午,产品找到我(为什么总感觉周五快下班就来活 ),跟我说有几个业务列表查询需求加上时刻条件过滤数据,这个条件或许会变,不确保以后不修正,这个改动涉及到多个列表查询,所以博主思考了一会想了几种完成方案,
- 最简略,直接将时刻条件写死,由 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 地址:github.com/wayn111/way…
阻拦器介绍
阻拦器是一种基于 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】每周共享技能干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!