SQL+C#+jQuery 实现非递归无限级分类

该文章发布于 2016年10月08日 03:19,星期六,by 何朝君,归档于 .NET C#。阅读354次,今日1次 扫码阅读 SQL+C#+jQuery 实现非递归无.. SQL+C#+jQuery 实现非递归无限级分类

摘要: 在本文中,先讲解无限级分类的逻辑联系,然后如何将这种逻辑联系转换成数据库上的物理存储。在理解的基础上,开始编写数据库SQL的CURD命令,然后编写C#代码连接操作数据库,最终使用jQuery控制C#读取数据的展示形式。该文章提供图片展示及代码下载。

SQL+C#+jQuery 实现非递归无限级分类

开发环境
开发工具:SQL2008 R2, VS2012
软件平台:微软WIN7操作系统,IIS7.5,.NET 4.0
所用插件:jQuery v1.11.3

该文章,最终所实现的效果。如下:
分类形式的效果

分类展示的效果

评论形式的效果

评论展示的效果

以下是我对无限级分类的分析。
比如用户所看到的无限级分类展示形式如下:

分类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邮箱。

(本篇完) // 有话要说?点击这里。想要打赏?点击这里

相关文章

标签:SQL, NET, jQuery

发表评论(目前0条评论)

名称
邮箱
网址
×意见反馈
标题
名称
邮箱
电话
公司
网址