[置顶] C#无限分类,递归重新生成排好序的树状数据源

时间:2022-07-24 11:41:19

现在有无限分类表结构如下:

create table OA_GoodType (
ID int not null,--ID字段,u也可以说是子ID
Name varchar(50) null,--名称
Pid int not null,--父类ID ,如果等于ID的时候就是*分类,(也可以设置为0的时候是*分类)
Path varchar(100) not null,--路径(父类的集合,全部都包含‘0,’开头,最后一位数字就是自身的id的值)
orderId int null,--排序
constraint PK_OA_GOODTYPE primary key (ID)
)
上面提到Path字段都是以 '0,' 开头,这样有个好处就是选出某个类(ID为3)下面的子类时直接可以用path like ',3,'
为表示结构关系,插入以下数据:
INSERT INTO OA_GoodType([Id],[Name],[Pid],[Path],[OrderId]) VALUES(1,'被服',1,'0,1',4)
INSERT INTO OA_GoodType([Id],[Name],[Pid],[Path],[OrderId]) VALUES(2,'提花被',1,'0,1,2',2)
INSERT INTO OA_GoodType([Id],[Name],[Pid],[Path],[OrderId]) VALUES(3,'军用棉被',1,'0,1,3',1)
INSERT INTO OA_GoodType([Id],[Name],[Pid],[Path],[OrderId]) VALUES(4,'军用棉被1.2米',2,'0,1,2,4',1)
INSERT INTO OA_GoodType([Id],[Name],[Pid],[Path],[OrderId]) VALUES(37,'救援工具类',37,'0,37',2)
INSERT INTO OA_GoodType([Id],[Name],[Pid],[Path],[OrderId]) VALUES(43,'粮油食品类',43,'0,43',6)

一般来说,对该结构的无限分类,要选出树状的结构图非常简单:只一条sql语句就够了:
select * from OA_GoodType order by path asc
但是这样的语句无法按照OrderID去排序
所以如果有要求排序的话,就必须对表进行递归了,
如下的方法,对数据按照排序生成一颗树状的DataTable:
/// <summary>  
/// dt 要从中生成树状数据源
/// IdField ID字段列名
/// ParentField 父级ID列名
/// Pid 父级ID值
/// OrderField 排序字段
/// </summary>
public DataTable GetTreeList(DataTable dt, string IdField, string ParentField, int Pid,string OrderField="")
{

DataTable newDT = dt.Clone();// 克隆dt 的结构,包括所有 dt 架构和约束,并无数据;
DataRow[] rows;

/*
* 如果父级pid等于本ID时表示*分类的情况 用以下语句
*/
if (Pid==0)//选出*的类别
{
rows = dt.Select(ParentField + "=" + IdField, OrderField); // 从dt 中查询符合条件的记录(选出所有大类);
}
else//选出属于所传ID的子类
{
rows = dt.Select(ParentField + "<>" + IdField + " and "+ParentField+"=" + Pid, OrderField); // 从dt 中查询符合条件的记录(选出大类pid的子类);
}


/*
* 如果父ID为0的时候表示*分类,用以下语句
* rows = dt.Select(ParentField + "=" + Pid, OrderField);
*/


if (rows.Length > 0)
{
foreach (DataRow row in rows) // 将查询的结果添加到dt中;
{
newDT.Rows.Add(row.ItemArray);
int innerpid = int.Parse(row["id"].ToString());
DataRow[] Childows = dt.Select(ParentField + "<>" + IdField + " and " + ParentField + "=" + innerpid, OrderField);//选出子类
if (Childows.Length > 0)//如果存在子类,对子类进行递归
{
DataTable innerDT = GetTreeList(dt, IdField, ParentField, innerpid, OrderField);//获取子类DataTable
foreach (DataRow innerrow in innerDT.Rows)//对子类的DataTable添加进总数据源
{
newDT.Rows.Add(innerrow.ItemArray);
}
}
}
}
dt.Dispose();//现不清楚是否需要该语句,执行该语句也正常显示
return newDT;
}

用法如下:

DataTable dt = comm_table.GetList("select * from OA_GoodType").Tables[0];//重组树状数据源,所以此处的SQL可以不用排序
subCate.DataSource = comm_table.GetTreeList(dt,"id","Pid",0,"OrderID");
subCate.DataBind();//subCate是Repeater控件

附上对于无限分类的表的一些SQL操作

1、选出某个类所属的父类路径(包括名称和id);对于本表而言,本方法一般是在列出属于某个类下面的物品详情的时候,顺便列出该物品所属的一系列分类

create proc getPathInfo          
@Id varchar(120), --需要选出路径名称的记录的id 或者路径
@IdField varchar(30), --存储ID的字段名
@NameField varchar(30), --存储名称的字段名
@PathField varchar(30), --存储路径的字段名
@Table varchar(30), --要操作的表名
@Split varchar(10)='→', --连接路径用到的分隔符
@isPath bit = 0 --传进来的是路径的话设置为1
as
DECLARE @sql nvarchar(300)
DECLARE @Path varchar(50) --存储父类路径的id集合
DECLARE @PathName varchar(100) --循环时存储各个父类名称(如果有的名称太长可适当增大该字段)
DECLARE @AllPath varchar(500) --存储最终各个父类名称形成的字符串(如果有的名称太长可适当增大该字段)
--DECLARE @index int
SET @AllPath=''
if(@isPath=1)--传进来的是路径的时候
SET @Path=@Id
else
begin
SET @sql = 'select @Path='+@PathField+' from '+@Table+' where '+@IdField+'='+@Id
EXEC sp_executesql @sql, N'@Path as varchar(50) OUTPUT', @Path OUTPUT
end
--print(@Path)
DECLARE @NameCursor cursor
set @sql=N'select '+@NameField+' from '+@Table+' where '+@IdField+' in('+@Path+') order by '+@PathField+' asc'-- 游标查询语句
-- 游标处理
set @sql = N'SET @NameCursor = CURSOR FORWARD_ONLY
FOR
'+@sql+N'
OPEN @NameCursor'
EXEC sp_executesql @sql, N'@NameCursor CURSOR OUTPUT', @NameCursor OUTPUT
fetch NEXT from @NameCursor into @PathName
while @@fetch_status=0
begin
SET @AllPath = @AllPath + @Split + @PathName--把路径用连接符连接起来,由于一开始的时候@AllPath 为空,所以再返回路径的时候要截取掉第一个分隔符
fetch NEXT from @NameCursor into @PathName
end;
--对于名字截取掉第一个分隔符,sqlserver的字符串下标以1开始,对于ID,所有的ID都包含了 0,所以要从第三个开始
select substring(@AllPath,len(@Split)+1,len(@AllPath)) as PathName,substring(@Path,3,len(@Path)) as PathID
close @NameCursor
deallocate @NameCursor
可用语句 exec getPathInfo 20,'id', 'name', 'path', 'OA_GoodType' 去测试以上语句的返回结果

二、选出某个类下面的所有子类id,本方法用处很多,例如一个分类下面有好多个分类,每个分类下面都包含了物品(对于文章系统的话可以说是文章),
那么一般来说点击父类的时候都需要列出属于该分类下面的所有文章,包括其子类或者子类下的子类;
create proc getChildInfo        
@Id varchar(100), --需要选出路径名称的记录的id (或者路径)
@IdField varchar(30), --存储ID的字段名
@NameField varchar(30), --存储名称的字段名
@PathField varchar(30), --存储路径的字段名
@Table varchar(30), --要操作的表名
@Split varchar(10)=',', --连接所有子类用到的分隔符
@isPath bit = 0 --传进来的是路径的话设置为1
as
DECLARE @sql nvarchar(300)
DECLARE @ChildID varchar(20) --存储父类路径的id集合
DECLARE @AllChild varchar(120)
SET @AllChild=''
-- 游标查询语句
if(@isPath=1)--传进来的是路径的时候
SET @sql = N'select '+@IdField+N' from '+@Table+N' where '+@PathField+N'='''+@Id+N''' or '+@PathField+N' like '''+@Id+N',%'''
else
SET @sql = N'select '+@IdField+N' from '+@Table+N' where '+@PathField+N' like ''%,'+@Id+N''' or '+@PathField+N' like ''%,'+@Id+N',%'''
DECLARE @IDCursor cursor
-- 游标处理
set @sql = N'SET @IDCursor = CURSOR FORWARD_ONLY
FOR
'+@sql+N'
OPEN @IDCursor'
EXEC sp_executesql @sql, N'@IDCursor CURSOR OUTPUT', @IDCursor OUTPUT
fetch NEXT from @IDCursor into @ChildID
while @@fetch_status=0
begin
SET @AllChild = @AllChild + @Split + @ChildID--把路径用连接符连接起来,由于一开始的时候@AllPath 为空,所以再返回路径的时候要截取掉第一个分隔符
fetch NEXT from @IDCursor into @ChildID
end;
--对于名字截取掉第一个分隔符,sqlserver的字符串下标以1开始,对于ID,所有的ID都包含了 0,所以要从第三个开始
select substring(@AllChild,len(@Split)+1,len(@AllChild)) as PathName
close @IDCursor
deallocate @IDCursor

可用语句 exec getPathInfo 1,'id', 'name', 'path', 'OA_GoodType' 去测试以上语句的返回结果

结束!!!!!!!!!



附:再分享一下无限分类查询、添加和修改的时候的一些操作:

/* 查询某个类(假设id是3)下的直接子类*/
Select * from OA_GoodType where Pid=3

/* 查询某个类(假设id是3)下的所有子类*/
Select * from OA_GoodType where pid=3 or path like '%,3,%'

添加和修改的时候所属大类的值都使用用path,如下:

页面上用select 框列出所有的分类:

如过要按照排序去列出来,那么就用上面的方法先排序,或者直接用:Select  * from OA_GoodType order by path asc

然后用这方法列出来一个梯级的select:(因为C#的丢失了,就用PHP的代替了:)假设数据源是一个数组:$cateArr

$options = '';
foreach($step_1[0] as $v){
$options .= '<option id="'.$v['id'].'" value="'.$v['path'].'">';
$cate_count = intval($v['level']);
if($cate_count > 1){
//如果有两级及以上
//将分类字段中的每一级分类替换为4个空格输出,设置缩进
$options .= str_repeat('  ',$cate_count-1);
$options .= '|--'.$v['name'].'</option>';
}else{
$options .= $v['name'].'</option>';
}
}
然后直接输出就可以了:

<select name="belong" id="belong" >
<option id="0" value="0">*分类</option>
<?php echo $options?>
</select>

显示如下:

[置顶]        C#无限分类,递归重新生成排好序的树状数据源


这样在保存的时候可以这样进行:

$belong = preg_replace('/[^\d|,]/','',$_POST['belong'];

$pid = 0;$level = 1;
$path = '';

//处理输入数据
if($belong && strpos($belong,',') === false){//如果有选择地区,但是地区不合法
MessageShowAndGoback('所属地区有误!');
}

if($belong == '0'){//是*分类的时候,路径就是 0,+本类id
$path_pre = '0,';
}else{//不是*分类的时候,路径就是父类路径 $belong +本类id
$path_pre = $belong.',';
$Arr = explode(',',$belong);
$pid = end($Arr);
$level = count($Arr);
}

$insertArr = array(//这里多了一个字段:level,好处就是:当你在只需要某一级分类的时候可以直接获得:select * from table where level=2;
'level'=>$level,
'pid'=>$pid,
'path'=>$path
);
//插入相应记录之后记着更新该记录的path字段!因为path字段的最后一个数字是本记录的id的值,这个值是自增的,所以只能现插入后更新:
//假设记录的自增id是:$pid
$sql = 'Update table set path = '.$path_pre.$id.' where id='.$id;

修改的时候比较复杂,要处理的问题比较多,例如:1、不能把本类的所属分类 修改成它的子类。2、要把本类的子类也一同转移到新的所属分类上,那就涉及把它的子类的path改成对应的新的path页面上列出的分类仍然是用path作为值:

(可以用一下语句设置给分类所属的分类:)

jQuery("#"+$id).selected=true;

js阶段判定是否违反1中所说条件:

把原来的path存储在一个叫old_path的隐藏域中

var path_old = form.old_path.value;
var path_new = form.belong.value;
if(path_new.indexOf(path_old+",") != -1){//移动到的分类中包含了原来的分类
alert('不能移动到本类的子分类下!');return false;
}

 

php保存的时候要做如下处理: 

$belong = preg_replace('/[^\d|,]/','',$this->input->post('belong'));
//获取原来的所属地区信息
$old_path = preg_replace('/[^\d|,]/','',$this->input->post('old_path'));
//根据路径获取原来分类所属地区
$parents_arr = explode(',',$old_path);
$old_pid = $parents_arr[count($parents_arr)-2];//存储原来的所属地区
$fixPath = false;
//判断是否有修改所属地区
//1、判断原来的所属分类和现在的所属分类是不是一致
$newParends = explode(',',$belong);
$newPID = end($newParends);
if($old_pid != $newPID){//原来的所属分类和现在的所属分类是不是一致
//也不是移动到本分类下
if($rid != $newPID){
$fixPath = true;

}
}

//1、如果有修改所属地区,需要对该地区下的所有分类进行转移,更改path以及level
if($fixPath){//有修改所属分类的时候
if(strpos($belong,$old_path) !== false){
MessageShowAndGoback('不能移动到本类的子分类下');
}

$Arr = explode(',',$belong);
$updatetArr['pid'] = end($Arr);
$updatetArr['level'] = count($Arr);//新的地区等级
$updatetArr['path'] = $belong.','.$rid;

$old_level = count($parents_arr)-1;//原来地区等级

}
if($fixPath){//有修改路径的时候,把他的子类全部都改过来

$where = ' (pid =\''.$rid.'\' or path like \''.$old_path.',%\')';
$newLevel = $updatetArr['level']-$old_level;
$rows = $this->AreaClassifys->Update(array(
'path'=>"replace(path,'$old_path','".$updatetArr['path']."')",
'level'=>"level+$newLevel"),$where,false);
//$sql = "update cms_category set path = replace(path,'$src_path','$new_path') where path like '$src_path%' or pid = $rid
}
完结