likes
comments
collection
share

水煮MyBatis(二八)- 级联插件【核心源码】

作者站长头像
站长
· 阅读数 12

前言

  上一章里说了,现实情况里,很难抽出大段完整时间来用于文章输出,只能切成小章来逐步完成既定目标,事实就是这样。
  好了话不多说,这是级联插件的最后一个小章,也是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;
    }

是否会循环级联呢?

既然提到了递归,不得不注意死循环的问题,比如下图这种互相依赖的场景。答案是不会,因为我们在结构树里,会判断父查询是否已经出现在路径里,如果出现过的实体,是不会继续级联的。 水煮MyBatis(二八)- 级联插件【核心源码】

处理逻辑:

  • 判断当前类字段的类型是否有数据库表映射,原始类型不会触发级联逻辑,比如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);
        }
    }

后记

这个系列就不独立写后记小结了,就这样,结束。