前言
稍有经验的后端开发者应该都有类似的小烦恼,在使用mybatis的时候,会比较羡慕JPA里的级联查询,使用类似@OneToMany之类的注解就能查询到与之关联的数据,非常方便。而在mybatis里,处理级联时,往往需要开发者自己使用sql语句来实现,效率低不说,而且毫无技术含量。
级联插件介绍安排
这个插件实现了在mybatis框架里的级联查询,逻辑上比数据权限插件复杂很多,需要分成下面几个部分来介绍。
-
第一章:插件里的数据结构,类似spark的有向无环图;
-
第二章:mybatis里执行原始查询语句,不绑定任何domain;
-
第三章:自定义注解;
- @ToMany,一对多
- @ToMiddleTable,多对多
- @ToOne,一对一
-
第四章:整体逻辑
循环查询
在进行级联查询的时候,往往多个表互相引用,造成循环查询,从而引发栈溢出异常。以下面这个经典例子来说,学生有指定的班主任,班级也有指定的班主任,用这个字段关联了下面的三张表。
查询班级时,会关联查处学生列表和班主任对象。如果继续关联,在学生对象里,又会去查询班级,然后就开始循环了。
图示中的红色连线是需要规避的。
数据结构
这是一个简单的树结构,根节点使用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;
}
无效节点图示
在下面的截图中,红色标出的节点,已在树中出现,所以不能继续关联分裂查询。
例子对应的几个类
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;
}