水煮MyBatis(二八)- 级联插件【核心源码】
前言
上一章里说了,现实情况里,很难抽出大段完整时间来用于文章输出,只能切成小章来逐步完成既定目标,事实就是这样。
好了话不多说,这是级联插件的最后一个小章,也是mybatis系列的最后一个篇章。之后准备写一个微服务相关的系列,篇幅未定,先立个flag吧。
核心功能源码
核心功能代码都在Interceptor的实现类里,对ResultSetHandler的handleResultSets方法进行拦截,有完整源码需要的可以单独联系我。
@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets",args = Statement.class))
@Slf4j
@Component
public class SelectCascadePlugin implements Interceptor { ... }
拦截入口
注意这个拦截入口,一个查询语句如果触发了级联,会递归执行下去;也就是说子查询里,如果也触发了级联,也会进入到这里,所以从CASCADE_TREE数据结构来说,会有许多棵级联树。 大概逻辑:
- 初始化级联树;
- 循环遍历级联树;
- 【隐藏逻辑】递归触发级联;
- 清理级联树;
public Object intercept(Invocation invocation) throws Throwable {
// 执行
Object result = invocation.proceed();
Object root = null;
// 记录开始级联查询的类,删除时判定是否当前类;因为插件是以递归的方式执行,startCls变量会被多次赋值
if (result != null) {
if (CASCADE_TREE.get() == null) {
root = new Object();
// 初始化
CASCADE_TREE.set(Node.init(root));
log.debug("CASCADE,init");
}
// 根据注解,判定是否需要进行级联查询
for (Object obj : ((List<?>) result)) {
selectCascade(obj, null);
}
// 递归回到最初的维度,删除线程中的缓存数据
if (root != null) {
// 清理,避免内存泄露
CASCADE_TREE.remove();
log.debug("CASCADE,clear");
}
}
return result;
}
是否会循环级联呢?
既然提到了递归,不得不注意死循环的问题,比如下图这种互相依赖的场景。答案是不会,因为我们在结构树里,会判断父查询是否已经出现在路径里,如果出现过的实体,是不会继续级联的。
处理逻辑:
- 判断当前类字段的类型是否有数据库表映射,原始类型不会触发级联逻辑,比如String,List等,直接跳过;
- 从当前节点上溯到根节点,如果当前类在这个路径里有出现,则表示已经处理过,直接返回;
- 否则加入到结构树里;
- 获取类里的所有字段,逐个处理级联;
/**
* 根据注解,判定是否需要进行级联查询
*
* @param obj 查询对象
* @throws Throwable 抛出异常
*/
private void selectCascade(Object obj, String parentNodeId) throws Throwable {
if (obj == null) {
return;
}
Class<?> cls = obj.getClass();
// 原始类不处理,比如int,map等。
Table table = cls.getAnnotation(Table.class);
if (table == null) {
log.debug("CASCADE,ignore,cls:{},parentNodeId:{}", cls, parentNodeId);
return;
}
Node root = CASCADE_TREE.get();
// 如果之前已经执行过查询,则拒绝此次级联查询
if (!isValid(root, parentNodeId, cls)) {
log.debug("CASCADE,CYCLE,cls:{},parentNodeId:{}", cls, parentNodeId);
return;
}
// 如果不存在,则记录
Node parentNode = getNode(root, parentNodeId);
if (parentNode == null) {
root.addChild(obj);
} else {
parentNode.addChild(obj);
}
log.debug("CASCADE,handle,cls:{},parentCls:{}", cls, parentNodeId);
// 获取类里的所有字段,逐个处理级联
for (Field field : cls.getDeclaredFields()) {
// 处理 @ManyToMany
manyToMany(obj, field);
// 处理 @OneToOne
oneToOne(obj, field);
// 处理 @OneToMany
oneToMany(obj, field);
}
}
多对多
不管是一对多、一对一、多对多,处理的方式都是相似的,就不啰嗦一个个介绍了,这里单独看看多对多的处理逻辑。 处理步骤:
- 判断是否有级联,如果无级联,则直接返回;
- 获取关联的目标类;
- 获取关联的注解;
- 拼接sql,也可以使用preStatement的方式来处理,更安全,这里偷懒了;
- 使用第27章介绍的MybatisUtil类处理原始查询;
- 将查询的结果,设置到目标POJO类实例的字段上;
- 进一步处理子级联;
private void manyToMany(Object obj, Field field) throws Throwable {
Class<?> cls = obj.getClass();
ToMiddleTable cascade = field.getAnnotation(ToMiddleTable.class);
if (cascade == null) {
// 如果无级联,直接返回
return;
}
// 获取关联的目标类
Class<?> targetCls = (Class<?>) ((ParameterizedTypeImpl) field.getGenericType()).getActualTypeArguments()[0];
Table targetTableAnnotation = targetCls.getAnnotation(Table.class);
String targetTable = targetTableAnnotation.name();
// 获取当前对象的id值
Object id = MybatisUtil.getId(obj);
String sql = "select * from " + targetTable + " where id in " +
"(select " + cascade.target() + " from "
+ cascade.table() + " where " + cascade.self() + " = " + id + ")";
log.debug("execute cascade sql:{}", sql);
// 级联查询
List<?> list = MybatisUtil.selectList(sql, targetCls);
// list判空
if (list != null && list.size() > 0) {
// 将list设置到对应属性
Method setMethod = MybatisUtil.getSetMethod(cls, field.getName());
// 设值
setMethod.invoke(obj, list);
// 处理子级联查询
handlerDepth(list, cls.toString() + id, cascade.depth());
}
}
判断是否需要深度级联
在我们的注解体系中,有个depth属性,如果需要深度级联,那么会对子查询进一步的级联下去,直到遇到注解里的depth=false,或者没有子查询为止。当前这是有一定风险的,如果级联过深,会造成OOM或者StackOverflowError。
/**
* 处理深度子级联查询
*
* @param list 查询结果
* @param depth 是否进行深度级联查询
* @throws Throwable t
*/
private void handlerDepth(List<?> list, String parentNodeId, boolean depth) throws Throwable {
// 是否进行深度级联查询
if (!depth) {
return;
}
// 处理子级联
for (Object childObj : list) {
selectCascade(childObj, parentNodeId);
}
}
后记
这个系列就不独立写后记小结了,就这样,结束。
转载自:https://juejin.cn/post/7251394142683529277