来源:蔡诗奔个人博客(http://www.caishiben.com)
本文全文转载需购买版权(1¥),摘要引流则免费,具体参见这里
摘要: 在本文中,先讲解无限级分类的逻辑联系,然后如何将这种逻辑联系转换成数据库上的物理存储。在理解的基础上,开始编写数据库SQL的CURD命令,然后编写C#代码连接操作数据库,最终使用jQuery控制C#读取数据的展示形式。该文章提供图片展示及代码下载。
该文章,最终所实现的效果。如下:
分类形式的效果
评论形式的效果
以下是我对无限级分类的分析。
比如用户所看到的无限级分类展示形式如下:
分类1 | ||
分类1-1 | ||
分类1-1-1 | ||
分类1-1-2 | ||
分类1-2 | ||
分类2 | ||
分类2-1 | ||
分类2-2 | ||
分类3 |
无限级分类概念上的逻辑联系,将展示形式转换成空间结构图,就会有横向(X轴)、纵向(Y轴)、分类排序(Z轴),简单表示为如下:
无限级分类数据库物理上的实际存储,将逻辑联系抽象出来,形成表的几个属性字段。
名称 | 根目录 | 横向深度 | 分类排序 | ||
分类1 | 1 | 0 | 0 | ||
分类1-1 | 1 | 1 | 1 | ||
分类1-1-1 | 1 | 2 | 2 | ||
分类1-1-2 | 1 | 2 | 3 | ||
分类1-2 | 1 | 1 | 4 | ||
分类2 | 2 | 0 | 0 | ||
分类2-1 | 2 | 1 | 1 | ||
分类2-2 | 2 | 1 | 2 | ||
分类3 | 3 | 0 | 0 |
符合以上的数据存储形式,结合JavaScript便可以完成非递归的无限级分类。事实上,这种数据存储形式除了符合无限级分类功能外,还可以完成文章评论,留言评论等其他功能。其他更多功能,有待广大读者一起交流实践。
理解无限级分类的存储形式后,可以着手编写SQL代码。
无限级分类SQL的文档说明及代码。
文档说明:
分类信息表( [Class_Info]) | ||||
字段名称 | 字段描述 | 字段类型 | 为空 | 默认值 |
[ClassId] | 分类编号 | uniqueidentifier | 否 | NewId() |
[ParentId] | 父类编号(外键) | uniqueidentifier | 否 | |
[ParentIdStr] | 父类编号字符串 | varchar(4000) | 否 | ‘’ |
[RootId] | 根目录编号 | int | 否 | 0 |
[Depth] | 类别横向深度 | smallint | 否 | 0 |
[Order] | 类别纵向深度 | int | 否 | 0 |
[VerCol] | 时间戳 | rowversion | ||
[AddDate] | 添加时间 | datetime | 是 | GETDATE() |
[Name] | 分类名称 | nvarchar(50) | 否 | ‘’ |
[PhotoId] | 图片编号 | varchar(250) | 否 | ‘’ |
[ReadMe] | 说明 | nvarchar(100) | 否 | ‘’ |
主键 | [ClassId] | |||
外键 | [ParentId]引用—分类信息表,有引用关系,不需创建外键 | |||
备注 | 分类信息表将行数据进行组划分,达到数据归类管理。 |
SQL代码:
创建数据库Exercise(如果不想创建数据库,可跳转到“创建数据库分类信息表”):
/* 创建----Exercise数据库*/ CREATE DATABASE Exercise ON PRIMARY( name='Exercise_pri', filename=N'F:\Database\Exercise_pri.mdf', size=200MB, maxsize=2048MB, filegrowth=200MB ) LOG ON( name='Exercise_log', filename=N'F:\Database\Exercise_log.ldf', size=100MB, maxsize=1024MB, filegrowth=100MB )
创建数据库分类信息表:
/* 创建----分类信息表*/ CREATE TABLE [dbo].[Class_Info]( [ClassId] uniqueidentifier not null CONSTRAINT [PK__Class_Info__ClassId] PRIMARY KEY, [ParentId] uniqueidentifier not null, [ParentIdStr] varchar(4000) not null CONSTRAINT [DF__Class_Info__ParentIdStr] DEFAULT(''), [RootId] int not null CONSTRAINT [DF__Class_Info__RootId] DEFAULT(0), [Depth] smallint not null CONSTRAINT [DF__Class_Info__Depth] DEFAULT(0), [Order] int not null CONSTRAINT [DF__Class_Info__Order] DEFAULT(0), [VerCol] rowversion, [AddDate] datetime not null CONSTRAINT [DF__Class_Info__AddDate] DEFAULT(GETDATE()), [Name] nvarchar(100) not null CONSTRAINT [DF__Class_Info__Name] DEFAULT(''), [ReadMe] nvarchar(100) not null CONSTRAINT [DF__Class_Info__ReadMe] DEFAULT('') )
创建分类信息表的CRUD代码。由于U的代码过于冗长,目前这里只放出CRD,代码如下:
if exists (select * from dbo.sysobjects where id = object_id(N'[proc_Class_Info]') and OBJECTPROPERTY(id,N'IsProcedure')= 1) drop procedure [proc_Class_Info] GO /* 创建----分类信息表包括CRUD的存储过程*/ CREATE PROCEDURE [proc_Class_Info] @ClassId uniqueidentifier = null, @ParentId uniqueidentifier = null, @ParentIdStr varchar(4000) = '', @RootId int = 0, @Depth smallint = 0, @Order int = 0, @AddDate datetime = null, @Name nvarchar(100) = '', @ReadMe nvarchar(100) = '', @ReturnInfo nvarchar(50)= '' output, @ReturnCode char(6)= '' output, @SPAction varchar(100)= null AS BEGIN --SET NOCOUNT ON; DECLARE @ClassIdString varchar(50), @EmptyGUID uniqueidentifier, @ChildCount int; --child count SET @EmptyGUID=CAST(CAST(0 AS binary) AS uniqueidentifier); -- 插入数据(Create) IF @SPAction='Insert' BEGIN SELECT @EmptyGUID AS [ClassId]; -- 检查父类是否存在 IF (@ParentId<>@EmptyGUID) AND NOT EXISTS(SELECT 1 FROM [Class_Info] WHERE [ClassId]=@ParentId) BEGIN SELECT @ReturnInfo=N'父类不存在',@ReturnCode=N'00000N'; RETURN -1; END -- 同级是否存在相同名称 IF EXISTS(SELECT 1 FROM [Class_Info] WHERE [ParentId]=@ParentId AND ([Name] = @Name)) BEGIN SELECT @ReturnInfo=N'数据重复',@ReturnCode=N'00000R'; RETURN -1; END DECLARE @RowCount int ,@errorCount int; -- 创建事务 BEGIN TRANSACTION InsertTran -- 作为根目录 IF (@ParentId = @EmptyGUID) BEGIN -- 根目录字符串 SET @ParentIdStr = CONVERT(varchar(MAX), @ParentId); -- 最大根目录 SELECT @RootId=ISNULL(MAX([RootId]), 0) FROM [Class_Info] WHERE [ParentId] = @ParentId; SELECT @RootId += 1, @Order=0, @Depth=0; END -- 作为子目录 ELSE BEGIN -- 查询父类信息 SELECT @ParentIdStr=[ParentIdStr],@RootId=[RootId] ,@Order=[Order], @Depth=[Depth] FROM [Class_Info] WHERE [ClassId]=@ParentId; -- 当前数据的父类字符串 SET @ParentIdStr += ',' + CONVERT(varchar(50), @ParentId); -- 获取最大排序 SELECT @Order=ISNULL(MAX([Order]), @Order) FROM [Class_Info] WHERE [ParentIdStr] LIKE '%' + CONVERT(varchar(50),@ParentId) + '%'; -- 更新当前插入位置之后的数据排序 UPDATE [Class_Info] SET [Order] += 1 WHERE [RootId]=@RootId AND [Order]>@Order; SELECT @Order += 1, @Depth += 1; END INSERT INTO [Class_Info]([ClassId],[ParentId],[ParentIdStr] ,[RootId],[Depth],[Order] ,[AddDate],[Name],[ReadMe]) VALUES(@ClassId,@ParentId,@ParentIdStr,@RootId,@Depth ,@Order,GETDATE(),@Name,@ReadMe); SELECT @RowCount=@@ROWCOUNT, @errorCount=@@ERROR; IF @RowCount > 0 AND @errorCount < 1 BEGIN SELECT @ClassId AS [ClassId]; SELECT @ReturnInfo=N'新增成功',@ReturnCode=N'000000'; COMMIT TRANSACTION InsertTran; RETURN 0; END BEGIN SELECT @EmptyGUID AS [ClassId]; SELECT @ReturnInfo=@errorCount,@ReturnCode=N'00000F'; ROLLBACK TRANSACTION InsertTran; RETURN -1; END END -- 初始化查询 IF @SPAction='Init' BEGIN SELECT TOP 1 [ClassId],[ParentId],[ParentIdStr] ,[RootId],[Depth],[Order],[VerCol],[AddDate] ,[Name],[ReadMe] FROM [Class_Info] WHERE [ClassId] = @ClassId; END -- 树状列表查询(Retrieve) IF @SPAction='Treeview' BEGIN SELECT c.[ClassId],c.[RootId],c.[Depth],c.[Order],c.[Name],c.[ReadMe] ,ISNULL(parent.[ReadMe],'') [ParentUserName] FROM [Class_Info] c LEFT JOIN [Class_Info] parent ON parent.[ClassId] = c.[ParentId] ORDER BY c.[RootId] ASC,c.[Order] ASC; END -- 删除数据(Delete) IF @SPAction='Delete' BEGIN -- 检查数据合法性 IF (@ClassId IS NULL OR @ClassId=@EmptyGUID) BEGIN SELECT @ReturnInfo=N'参数缺失',@ReturnCode=N'00000L'; RETURN -1; END SELECT @RootId=[RootId] FROM [Class_Info] WHERE [ClassId]=@ClassId; IF @@ROWCOUNT <> 1 BEGIN SELECT @ReturnInfo=N'数据不存在',@ReturnCode=N'00000L'; RETURN -1; END SET @ClassIdString = CONVERT(varchar(50), @ClassId); -- 创建删除事务 BEGIN TRANSACTION DeleteTran; SET @ClassIdString = CONVERT(varchar(50), @ClassId); -- 查询最大排序及子类总数 SELECT @Order=MAX([Order]),@ChildCount=COUNT(1) FROM [Class_Info] WHERE [ClassId]=@ClassId OR [ParentIdStr] LIKE '%' + @ClassIdString + '%'; -- 删除子类 DELETE FROM [Class_Info] WHERE [ParentIdStr] LIKE '%'+ @ClassIdString + '%'; SELECT @RowCount=@@ROWCOUNT, @errorCount=@@ERROR; IF @errorCount>0 BEGIN SELECT @ReturnInfo=N'删除子类失败',@ReturnCode=N'00000F'; ROLLBACK TRANSACTION DeleteTran; RETURN -1; END -- 删除当前本身数据 DELETE FROM [Class_Info] WHERE [ClassId]=@ClassId; IF @@ROWCOUNT<>1 BEGIN SELECT @ReturnInfo=N'删除失败',@ReturnCode=N'00000F'; ROLLBACK TRANSACTION DeleteTran; RETURN -1; END -- 如果根目录还存在 IF EXISTS(SELECT 1 FROM [Class_Info] WHERE [RootId]=@RootId) BEGIN UPDATE [Class_Info] SET [Order] -= @ChildCount WHERE [RootId]=@RootId AND [Order]>@Order; IF @@ERROR>0 BEGIN SELECT @ReturnInfo=N'更新排序失败',@ReturnCode=N'00000F'; ROLLBACK TRANSACTION DeleteTran; RETURN -1; END END -- 如果根目录不存在 ELSE BEGIN -- 大于根目录的递减1 UPDATE [Class_Info] SET [RootId]-= 1 WHERE [RootId]>@RootId; IF @@ERROR>0 BEGIN SELECT @ReturnInfo=N'更新目录失败',@ReturnCode=N'00000F'; ROLLBACK TRANSACTION DeleteTran; RETURN -1; END END SELECT @ReturnInfo=CONVERT(nvarchar(20),(@RowCount+1))+N'条删除成功',@ReturnCode=N'000000'; COMMIT TRANSACTION DeleteTran; RETURN 0; END END
该项目所有SQL的CRUD代码可以通过点击这里下载。
有了SQL的代码后,创建ASP.NET C#代码连接数据库,调用存储过程@SPAction='Treeview'读取“分类信息表”数据。当前项目将读取到的DataTable转换成泛类型,再返回Json格式数据到前端。
C# MVC的代码功能大概如下:
SqlHelper类负责直接连接操作SQL数据库;
ClassDAO类负责将数据与执行的SQL命令结合,再调用SqlHelper类对数据库进行实际操作;
ListHelper类将读取到DataTable将成泛类型; 详细代码这里就不一一粘贴出来了,有意着了解的详情者,请点击这里下载。
Javascript的功能讲解:
Javascript主要是将View数据更直观地展示在用户面前,至于展示效果可以千变万化,读者可自行修改Js达到想要的效果。这里自定义一个jQuery插件,插件利用ajax读取数据,TreeViewTop方法根据横向深度,显示不同形式的Html。
代码如下:
(function ($) { $.fn.bindClassList = function (options) { var defaults = { url: '', mark: 'cate_' }, o = $(this); var opts = $.extend({}, defaults, options), html, differentDepth; function TreeViewTop(result) { html = ""; differentDepth = 0; for (var i = 0; i < result.length; i++) { if (i == (result.length - 1)) { differentDepth = 0 - result[i].Depth; } else { differentDepth = result[i + 1].Depth - result[i].Depth; } var htmlList = [ '<div class="item">' , '<a style="padding-left:' + (result[i].Depth > 0 ? 40 : 0) + 'px;" title="' , '">' + result[i].ReadMe + (!!result[i].ParentUserName ? ' 回复 ' + result[i].ParentUserName : '') + ':<b class="' + ((differentDepth == 1) ? "downcaret" : "rightcaret nochildren") + '"></b>' , result[i].Name ,'</a></div>' ] html += htmlList.join(''); if (differentDepth == 1) { html += '<div class="child"> '; } for (var j = differentDepth; j < 0; j++) { html += "</div>"; } } o.append(html); }; $.ajax({ type: 'POST', url: opts.url, data: {}, dataType: 'json', cache: false, success: function (result) { if (result.MessageCode == "000000") { TreeViewTop(result.rows); } else { window.top.exceptionInfo(result.MessageTip); } }, error: function (XmlHttpRequest, textStatus, ErrorThrow) { } }); return o; }; })(jQuery);
// 加载完成后,调用自定义插件bindClassList
$(function () { $('#treeview_list').bindClassList({ url: '/class/Treeview' });; });
用户所看到的无限级分类展示形式效果,事实都是JS依据横向深度处理出来的。
完整的项目可以通过点击这里下载。
总结:
清楚了解无限级分类的逻辑联系(横向、纵向、排序),再设法将这种逻辑联系转换成数据库物理上存储。一整条流程走下来后,主要麻烦的地方在于编写无限级分类SQL的CRUD命令,只要弄清楚了逻辑联系,实现也就是时间的问题。事实我本身也觉得数据库的CRUD写得比较冗余,望哪位读者能写出更简洁的SQL命令。好了,无限级分类到此打完收功,有疑问的读者,请点这里留言,或者将疑问发到1206910050@qq.com邮箱。
上一篇:在线静态页面----港联企业网站
下一篇:JSHOP-PC店铺装修
相关文章