在项目中,大家可能会遇到这样一个问题,就是当你操作那些具有上下级的树的表的时候,如果是单纯的父子级关系,可能不会碰见这个问题,但是如果这个看起来具有树形结构的表形成闭环的时候,问题就来了,我遇到的结果就是,一旦碰到这样的情况,就是页面一直卡在那里,对应着后台就是,要么程序死循环,要么数据库死循环,直到系统卡死崩溃。
这个问题很是头疼,在网上找了不少资料没有发现真正能够解决我的问题的,究其原因,还是这样的树形结构用的少,平常大家使用的多的还是那种无限层级的,也就是父子级可以无限下去的,这里就来看看我这个问题和解决办法吧。
首先数据库里面有一张表,其中有两个很关键的字段,targetIden和srcIden,这两个字段用于标记理论上可作为父子级的一条关系数据,可以理解成targetIden是父节点数据,srcIden是子节点数据,如图所示,
在这个表中我创建了几条测试数据,相信大家可以看出来,已经形成了一条联试结构的数据,也就是两两之间存在某种隐含的父子级关系,现在假如来了一个需求,需要创建一条尾号为 …55 和 …411的数据,也就是插入一条这样的数据,可以吗?
这个很简单,这需要执行一下插入即可,没问题,可以插入成功,问题就在这时候产生了,当这条数据插入成功后,我们在进行查询,比如以411为条件查询所有的关系数据,这时候悲催的问题就产生了,不管是用程序递归还是数据库的递归来查询,都会造成死循环,其实两者查询本质一样,都是递归,但是结果都是死循环儿查不出结果来,因为从数据结构上来说,形成了一条闭环的数据链路;
解决的思路无非有两种,
1、通过其他的方式查询出这个链路的数据【这个我想了不少办法,还是没有很好的解决方式】
2、插入数据的时候将这个问题规避掉;
对比了一下实现成本,第二张方式相对简单一点,下面就用第二种方式实现以下;
既然选择了第二种思路,我们先思考一下,在插入数据的时候,我们该怎么避免这个死循环的结构数据产生呢?
还是以上面的用555和411为例,我们插入411和555的关系之前,对于555这条数据,是不是需要把插入之前555的整个上下链路的所有数据查出来,查出来以后呢?只需要判断一下对应的411这条数据是否存在于555的前置链路或者后置链路上不久可以了吗
解决方法就在此,插入之前,我们把555之前的链路数据全部查询出来,555之后的数据全部查出来,然后基本上就出来了,下面看具体代码,主要就是一个方法类,
/**
* 判断是否存在死循环节点
* @param params
* @return
*/
public boolean isDeadNodes(Map params){
String targetIden = params.get("targetIden").toString();
String srcIdne = params.get("srcIden").toString();
List<String> exists = this.baseDao.queryForListBySql("fieldanalysisMapping.existTreesOld",params);
if(exists != null && exists.size() >0){
//存在关联的数据了,不允许存在
return true;
}
//如果父节点为空,可以直接进行插入数据
//1、查找父节点是否已经存在了?
List<String> targetIds = this.baseDao.listHql("select id from MdRelation where targetIden = '"+targetIden+"' or srcIden = '"+targetIden+"'");
List<String> beforesLeft = new ArrayList<>();
List<String> aftersRight = new ArrayList<>();
if(targetIds == null || targetIds.size() ==0){
// 父节点根本不存在
return false;
}else {
//父节点存在,查找父节点的所有前置节点 === 影响关系数据
Map beforeMap = new HashMap();
beforeMap.put("fieldIden",targetIden);
List<Map<String,String>> beforList =
this.baseDao.queryForListBySql("fieldanalysisMapping.getFieldsEffectsForFunction", beforeMap);
if(beforList != null && beforList.size() >0){
for(Map one:beforList){
beforesLeft.add(one.get("TARGET_IDEN").toString());
beforesLeft.add(one.get("SRC_IDEN").toString());
}
}
//再找到后置节点数据
Map afterMap = new HashMap();
afterMap.put("fieldIden",targetIden);
List<Map<String,String>> aftersList =
this.baseDao.queryForListBySql("fieldanalysisMapping.getFieldsBloodsForFunction", beforeMap);
if(aftersList != null && aftersList.size() >0){
for(Map one:aftersList){
aftersRight.add(one.get("TARGET_IDEN").toString());
aftersRight.add(one.get("SRC_IDEN").toString());
}
}
}
List<String> srcIds = this.baseDao.listHql("select id from MdRelation where targetIden = '"+srcIdne+"' or srcIden = '"+srcIdne+"'");
//如果子节点存在,
if(srcIds == null || srcIds.size() ==0){ //子节点根本不存在
return false;
}else{
//子节点存在了,判断是否存在于父节点的前置或者后置节点上,影响关系是后置,血缘关系是前置
if(beforesLeft == null || beforesLeft.size() ==0){
//如果前置节点为空
if(aftersRight != null && aftersRight.size() >0){
//后置节点存在
if(aftersRight.contains(srcIdne)){
return true;
}else{
return true;
}
}
}else{ //如果前置节点不为空
if(aftersRight==null ||aftersRight.size()==0){ //如果后置节点为空
if(beforesLeft.contains(srcIdne)){
return true;
}else{
return false;
}
}else{ //前后节点都存在
if(aftersRight.contains(srcIdne) && !beforesLeft.contains(srcIdne)){
return false;
}
if(!aftersRight.contains(srcIdne) && beforesLeft.contains(srcIdne)){
return true;
}
}
}
}
return false;
}
方法中对应的几个查询语句的mybatis方法也列举在下面,这里采用了数据库递归的方式,即采用了with - as函数的写法,分别查询555这条数据的前置和后置节点,
后置节点数据,
<select id="getFieldsBloodsForFunction" parameterType="java.util.Map" resultType="java.util.Map">
WITH PPL (ID,TARGET_IDEN,SRC_IDEN,RULE_TYPE,RULE_DESC) AS
(
SELECT ID,TARGET_IDEN,SRC_IDEN ,RULE_TYPE,RULE_DESC FROM MD_RELATION WHERE TARGET_IDEN = #{fieldIden}
UNION ALL
SELECT child.ID,child.TARGET_IDEN,child.SRC_IDEN ,child.RULE_TYPE, child.RULE_DESC FROM PPL parent,
MD_RELATION child WHERE child.TARGET_IDEN = parent.SRC_IDEN
)
SELECT * FROM PPL
</select>
前置节点数据:
<select id="getFieldsEffectsForFunction" parameterType="java.util.Map" resultType="java.util.Map">
WITH PPL (ID,TARGET_IDEN,SRC_IDEN,RULE_TYPE,RULE_DESC) AS
(
SELECT ID,TARGET_IDEN,SRC_IDEN ,RULE_TYPE,RULE_DESC FROM MD_RELATION WHERE SRC_IDEN = #{fieldIden}
UNION ALL
SELECT child.ID,child.TARGET_IDEN,child.SRC_IDEN ,child.RULE_TYPE, child.RULE_DESC FROM PPL parent,
MD_RELATION child WHERE child.SRC_IDEN = parent.TARGET_IDEN
)
SELECT * FROM PPL
</select>
因为我们的目标数据是555和411的关系,所有555的前置和后置的关系数据结构就显示成如上的结构,然后我们调用一下接口看看效果,可以看到我们已经将这条数据拦截到了,同样,我们再测试一个,
我们使用96和38这条数据测试,因为他们也是可能造成死循环的树,
可以看到也拦截到这对可能造成死循环的数据了,
通过上面的方式基本上达到了预期的效果,希望对看到的小伙伴有所有帮助!