水煮MyBatis(二六)- 级联插件【数据结构】

63 阅读2分钟

前言

  稍有经验的后端开发者应该都有类似的小烦恼,在使用mybatis的时候,会比较羡慕JPA里的级联查询,使用类似@OneToMany之类的注解就能查询到与之关联的数据,非常方便。而在mybatis里,处理级联时,往往需要开发者自己使用sql语句来实现,效率低不说,而且毫无技术含量。

级联插件介绍安排

这个插件实现了在mybatis框架里的级联查询,逻辑上比数据权限插件复杂很多,需要分成下面几个部分来介绍。

  • 第一章:插件里的数据结构,类似spark的有向无环图;

  • 第二章:mybatis里执行原始查询语句,不绑定任何domain;

  • 第三章:自定义注解;

    • @ToMany,一对多
    • @ToMiddleTable,多对多
    • @ToOne,一对一
  • 第四章:整体逻辑

循环查询

在进行级联查询的时候,往往多个表互相引用,造成循环查询,从而引发栈溢出异常。以下面这个经典例子来说,学生有指定的班主任,班级也有指定的班主任,用这个字段关联了下面的三张表。

image.png

查询班级时,会关联查处学生列表和班主任对象。如果继续关联,在学生对象里,又会去查询班级,然后就开始循环了。
图示中的红色连线是需要规避的。
image.png

数据结构

这是一个简单的树结构,根节点使用init方法构建,然后根据级联查询的深度,动态添加子节点。

    @Data
    public static class Node implements Comparable<Node> {
        /**
         * data的hashcode
         */
        private String id;
        /**
         * data的Class
         */
        private Class<?> cls;

        private Node(Object o, Object dbId) {
            id = o.getClass().toString() + dbId;
            cls = o.getClass();
        }

        /**
         * 初始化
         *
         * @param root 根对象
         * @return node
         */
        public static Node init(Object root) {
            return new Node(root, 0);
        }

        /**
         * 下级节点集合
         */
        private List<Node> children = new ArrayList<>();

        public void addChild(Object o) {
            // 获取当前对象的id值
            Object dbId = MybatisUtil.getId(o);
            children.add(new Node(o, dbId));
        }

        @Override
        public int compareTo(Node o) {
            return id.compareTo(o.id);
        }
    }

结构如何使用

需要遍历整棵树,如果在当前路径中,存在要查询的类,则返回false。也就是说,从最后一个子节点进行溯源遍历,没有一个不会出现重名的类,从而完美规避了循环查询的异常。

   /**
     * 判定是否可以进行查询
     *
     * @param node         根节点
     * @param parentNodeId 当前parent对象
     * @param searchCls    要查询的类
     * @return bool
     */
    public boolean isValid(Node node, String parentNodeId, Class<?> searchCls) {
        if (parentNodeId == null) {
            return true;
        }
        // 父节点是根节点,则直接通过
        if (Objects.equals(parentNodeId, node.getId())) {
            return true;
        } else {
            for (Node child : node.getChildren()) {
                // 找到对应节点
                if (Objects.equals(parentNodeId, child.getId())) {
                    return true;
                } else if (Objects.equals(searchCls, child.getCls())) {
                    // 如果在当前路径中,存在要查询的类,则返回false
                    return false;
                } else {
                    // 递归查找
                    return isValid(child, parentNodeId, searchCls);
                }
            }
        }
        return true;
    }

无效节点图示

在下面的截图中,红色标出的节点,已在树中出现,所以不能继续关联分裂查询。

image.png

例子对应的几个类


class Student{
	private int id;
	// 名称
	private String name;
	// 生日
	private int birthday;
	// 班主任id
	private int classChargeId;
    // 班级id
	private int schoolClassId;

	// 班主任
	@ToOne
	private Teather classCharge;
    // 班级
	@ToOne
	private SchoolClass schoolClass;

}

class Teather{
	private int id;
	// 名称
	private String name;
	// 生日
	private int birthday;
	// 薪资
	private int salary;
    // 班级id
	private int schoolClassId;
    
	@ToOne
	private SchoolClass cls;

}

class SchoolClass{
	private int id;
	// 名称
	private String name;
	// 班主任
	private int classChargeId;

	// 班级学生列表
	@ToMany
	private List<Student> students;

	// 班主任对象
	@ToOne
	private Teather classCharge;

}