zoukankan      html  css  js  c++  java
  • 前后端分离的轻量级内容管理框架

    分享一个前后端分离的轻量级内容管理框架(第二篇前后端交互数据结构分析)

     

    这是 CMS 框架系列文章的第二篇,第一篇开源了该框架的代码和简要介绍了框架的目的、作用和思想,这篇主要解析如何把sql 转成标准 xml 配置文件和把前端post的增删改数据规范成方便后台解析的结构,以实现后端自动化操作数据库。

    【开源.NET】 分享一个前后端分离的轻量级内容管理框架(第一篇)
    【开源.NET】 分享一个前后端分离的轻量级内容管理框架(第二篇前后端交互数据结构分析)

    信息管理系统

    信息管理系统关键功能:列表分页和搜索、方便数据展示和录入。业务复杂度通常在于多表关联的搜索、录入以及表与表和字段与字段之间的约束,还有就是报表统计了。除去报表不说,其它功能其实就是对数据库表进行增删改查,它们是独立于业务存在的,所以可对它们进行规范化和自动化。
    信息管理系统就是为了方便数据的展示和录入,简化为 “需求数据 - SQL - 数据库”, SQL 作为“需求数据” 与“数据库”的中介。想要自动化增删改查,必须要规范化“需求数据的结构”以及添加规范化的“配置文件”,用程序对它们进行分析以生成中介“SQL”。

    一、自动化搜索和分页

    1、设计图

    2、Sql查询转换成规范化的“配置文件”

    配置文件是手动配置,由后端程序处理的,不涉及到传输,所以应该选择 XML 格式,可读性和性能都可达到平衡。
    先看一下一条简单的 select 语句: Select main.* From VideoMain Where main.IsDeleted != 1,这里有 3 个关键字"Select, From, Where", 它们与业务无关,抽出来,组成xml:

    <Select>
      main.*
    </Select>
    <From>
      VideoMain main
    </From>
    <Where>
      main.IsDeleted != 1
    </Where>

    看去非常直观,和写 sql 没多大区别。
    再看一条复杂点的 select 语句:

    Select main.*, owner.Name as _OwnerName
    From VideoMain 
    Left Join BasOwner owner On owner.Id = main.OwnerId
    Where main.IsDeleted != 1 And main.Name like '%教程%'

    转成xml配置

    <Select>
      main.*, owner.Name as _OwnerName
    </Select>
    <From>
      VideoMain 
      Left Join BasOwner owner On owner.Id = main.OwnerId
    </From>
    <Where>
      main.IsDeleted != 1 And main.Name like '%教程%'
    </Where>

    不好的是条件写死了,但条件是由前端返回的,应该是动态的。其实 where 条件是由比较符号“=,!=, >, < , >=, <=, like, in, between and” 和逻辑“And, Or”组合起来的 ,可对它们进行规范化。
    首先把比较符号转换成有意义的单词,方便配xml,

    equal   : =
    notequal: !=
    bigger  : >=
    smaller : <=
    like    : like
    in      : in

    重新规范Where:

    <Select>
      main.*, owner.Name as _OwnerName
    </Select>
    <From>
      VideoMain 
      Left Join BasOwner owner On owner.Id = main.OwnerId
    </From>
    <Where>
      <Fields>
        <Field Name="IsDeleted" Prefix="main" Cp="notequal"></Field>
        <Field Name="Name" Prefix="main" Cp="like"></Field>
      </Fields>
    </Where>

    这样把一条查询的 sql 规范成xml,然后写程序进行解析,就容易了。

    3、把规范化的 xml 转换成标准的 sql

    看一段解析条件比较的代码:

    public string GetSql(string cp, string paraName, string dbname, string value, string sqlExpress = null, string dataType = null, bool isAnd = true, bool isAddQuotes = true)
    {
        string sql = "";
        if (!string.IsNullOrEmpty(sqlExpress))
        {
            if (isAddQuotes)
            {
                sql = sqlExpress.Replace("@" + paraName, "'" + ParseValue(value, dataType) + "'");
            }
            else
            {
                //sql = sqlExpress.Replace("@" + paraName, ParseValue(value, dataType));
                sql = sqlExpress.Replace("@" + paraName, "'" + ParseValue(value, dataType) + "'");
            }
        }
        else
        {
            value = string.IsNullOrEmpty(dataType) ? value : ParseValue(value, dataType);
            string orAnd = isAnd ? "And" : "Or";
            switch (cp.ToLower())
            {
                case "equal":
                    sql = Equal(dbname, value, orAnd);
                    break;
                case "like":
                    sql = Like(dbname, value, orAnd);
                    break;
                case "notequal":
                    sql = NotEqual(dbname, value, orAnd);
                    break;
                case "daterange":
                    sql = DateRange(dbname, value, orAnd);
                    break;
                case "bigger":
                    sql = Bigger(dbname, value, orAnd);
                    break;
                case "smaller":
                    sql = Smaller(dbname, value, orAnd);
                    break;
                case "in":
                    sql = In(dbname, value, orAnd);
                    break;
            }
        }
        return sql;
    }
    
    
    
    public string In(string fieldName, string value, string orAnd = "And")
    {
        return string.Format(" {2} {0} in ('{1}')", fieldName, FilterSql.FilterValue(value).Replace(",", "','"), orAnd);
    }
    public string Equal(string fieldName, string value, string OrAnd = "And")
    {
        return string.Format(" {2} {0} = '{1}'", fieldName, FilterSql.FilterValue(value), OrAnd);
    }
    public string Like(string fieldName, string value, string OrAnd = "And")
    {
        return string.Format(" {2} {0} like '%{1}%'", fieldName, FilterSql.FilterValue(value), OrAnd);
    }
    public string NotEqual(string fieldName, string value, string OrAnd = "And")
    {
        return string.Format(" {2} {0} <> '{1}'", fieldName, FilterSql.FilterValue(value), OrAnd);
    }
    public string Bigger(string fieldName, string value, string OrAnd = "And")
    {
        return string.Format(" {2} {0} >= '{1}'", fieldName, FilterSql.FilterValue(value), OrAnd);
    }
    public string Smaller(string fieldName, string value, string OrAnd = "And")
    {
        return string.Format(" {2} {0} <= '{1}'", fieldName, FilterSql.FilterValue(value), OrAnd);
    }
    

    有兴趣的,下载源码看,解析 xml 的代码在 Core/Easy.DataProxy 项目下。

    4、前端请求:
    <div class="dh-form">
        <div class="row">
            <div class="col2">菜单编码</div>
            <div class="col2"><input class="easyui-uc_validatebox" data-bind="value:form.Code" /></div>
            <div class="col2">菜单名称</div>
            <div class="col2"><input class="easyui-uc_validatebox" data-bind="value:form.Name" /></div>
            <div class="col1"><a class="easyui-uc_linkbutton" data-bind="click:_query()">搜索</a></div>
        </div>
    </div>

    form.Code 和 form.Name 对应后台配置文件的 Where:

      <Where>
        <Fields>
          <Field Name="Code" Prefix="main" Cp="like"></Field>
          <Field Name="Name" Prefix="main" Cp="like"></Field>
        </Fields>
      </Where>
    请求 /Bas/Menu/list?pageNumber=1&pageSize=20

    请求 /Sys/Menu/list?Code=sys&Name=系统&pageNumber=1&pageSize=20

    二、自动化增删改

    1、设计图

    将前端返回合法的 json 数据结合后台配置好的xml, 经由 EasyCore 解析生成标准的 sql。

    2、单表

    假设有张视频表,后台管理员可对它进行增删改操作。

    在界面“新增或编辑”,点“保存”后,将数据组织成 json 格式 POST 回后台,后台程序解析该 json, 生成 sql 对数据库相应的表进行增删改。
    这里有 3 个关键要素: 1.对应的表, 2.对表的操作类型(增、删、该),3.表字段的值。
    post 回后台的 json 结构大概是: {tableName:VideoMain, operation:'inserted', data:{Id:1, Name:"test1"}}
    如果要兼容批量操作的 json 结构是:

    [
    {tableName:VideoMain, operation:'inserted', data:{Id:1, Name:"test1"}},
    {tableName:VideoMain, operation:'inserted', data:{Id:2, Name:"test2"}},
    {tableName:VideoMain, operation:'updated', data:{Id:3, Name:"test3"}},
    {tableName:VideoMain, operation:'updated', data:{Id:4, Name:"test4"}},
    ]

    出现好多重复的标签: tableName, operation, data, 抽取出来,简化成:

    {
    tableName:{
        inserted:[{Id:1, Name:"test1"},{Id:2, Name:"test2"}],
        updated:[{Id:3, Name:"test3"},{Id:4, Name:"test4"}]
      }
    }
    
    3、主从表

    假设用户可对视频单独进行“评论”或“评分”, 还可以对“评论”进行“顶、踩”,而后台管理员可对它们进行增删改操作,简化的表结构如下:

    这就是一个典型的“主 - 从 - 从”的表结构,这里比单表多了一从信息:子表,json 结构如下:

    {
    VideoMain:{
        inserted:[
         { 
            data:{Name:"test1"},
            children:{
              VideoMark:{
                  inserted:[
                    {
                      data:{Mark:5}
                      },
                    {
                      data:{Mark:4}
                      }
                          ]
                         },
               VideoComment:{
                  inserted:[
                    {
                      data:{Comment:'good!'}, 
                      children:{
                         VideoCommentUpdown:{
                              inserted:[
                                {
                                  data:{IsUp:true}
                                  }
                                ]
                            }
                         }
                      },
                      {                                  {
                      data:{Comment:'very good!'}, 
                      children:{
                         VideoCommentUpdown:{
                              inserted:[
                                {
                                  data:{IsUp:true}
                                  }
                                ]
                            }
                         }
                      }
                    }
                    ]
                   },
                }
          },
          updated:[
            {
              data:{Id:3, Name:"test3"}
            }
          ]
        ]
      }
    }
    

    json 结构难以看出它的规律,把它换成脑图:

    其实就是一颗树,提取它的结构:

    从图可看出,树的差异结构最深层级为 4 级,4级之后又从 1 开始。 每一级的意义:

    • 第一级,表名: master/child1/child2;
    • 第二级,对表的操作类型: inserted/updated/deleted;
    • 第三级,批量操作的记录集合(数组);
    • 第四级,左节点:记录的数据,右节点:该条记录的子表数据,子表数据结构重复着 1-4级别;

    该 json 结构已很好的携带业务数据信息了,但并不完整,自增字段、主键、外键等约束信息和更新或删除所需的逻辑条件都没有,这些关系到数据库安全的信息不可能开放给前端去配的,所以还需要后台作相关的配置。

    4、后台配置
    </SqlConfig>
      <Table>VideoMain</Table>
      <ID>Id</ID>
      <PKs>Id</PKs>
      <Insert>
        <Fields>
          <Field Name="CreatedDate" IsIgnore="true"></Field>
          <Field Name="UpdatedDate" IsIgnore="true"></Field>
        </Fields>
      </Insert>
      <Update>
        <Fields>
          <Field Name="CreatedDate" IsIgnore="true"></Field>
          <Field Name="UpdatedDate" IsIgnore="true"></Field>
        </Fields>
        <Where>
          <Fields>
            <Field Name="Id" Cp="equal"></Field>
          </Fields>
        </Where>
      </Update>
      <Delete>
        <DeleteAnyway>false</DeleteAnyway>
        <Where>
          <Fields>
            <Field Name="Id" Cp="equal"></Field>
          </Fields>
        </Where>
      </Delete>
      <Children>
        <SqlConfig>
          <Table>VideoMark</Table>
          <JsonName>marks</JsonName>
          <ID>Id</ID>
          <PKs>Id</PKs>
          <Dependency>
            <Fields>
              <Field Name="MainId" DependencyName="Id"></Field>
            </Fields>
          </Dependency>
          <Update>
            <Where>
              <Fields>
                <Field Name="Id" Cp="equal"></Field>
              </Fields>
            </Where>
          </Update>
          <Delete>
            <Where>
              <Fields>
                <Field Name="Id" Cp="equal"></Field>
              </Fields>
            </Where>
          </Delete>
        </SqlConfig>
        <SqlConfig>
          <Table>VideoComment</Table>
          <JsonName>comments</JsonName>
          <ID>Id</ID>
          <PKs>Id</PKs>
          <Dependency>
            <Fields>
              <Field Name="MainId" DependencyName="Id"></Field>
            </Fields>
          </Dependency>
          <Update>
            <Where>
              <Fields>
                <Field Name="Id" Cp="equal"></Field>
              </Fields>
            </Where>
          </Update>
          <Delete>
            <Where>
              <Fields>
                <Field Name="Id" Cp="equal"></Field>
              </Fields>
            </Where>
          </Delete>
          <Children>
            <SqlConfig>
              <Table>VideoCommentUpdown</Table>
              <JsonName>commentUpdowns</JsonName>
              <ID>Id</ID>
              <PKs>Id</PKs>
              <Dependency>
                <Fields>
                  <FieldName="CommentId"DependencyName="Id"></Field>
                </Fields>
              </Dependency>
              <Update>
                <Where>
                  <Fields>
                    <FieldName="Id"Cp="equal"></Field>
                  </Fields>
                </Where>
              </Update>
              <Delete>
                <Where>
                  <Fields>
                    <FieldName="Id"Cp="equal"></Field>
                  </Fields>
                </Where>
              </Delete>
            </SqlConfig>
          </Children>
        </SqlConfig>
      </Children>
    </SqlConfig>

    SqlConfig xml 对应的对象

    public class SqlConfig
    {
        public SqlConfig()
        {
            this.Where = new Where();
            this.Children = new List<SqlConfig>();
            this.OrderBy = new OrderBy();
            this.GroupBy = new GroupBy();
            this.Dependency = new Dependency();
            this.Insert = new Insert();
            this.Update = new Update();
            this.Delete = new Delete();
            this.SingleQuery = new SingleQuery();
            this.Export = new Export();
            this.Import = new Import();
            this.BillCodeRule = new BillCodeRule();
        }
    
        private string _settingName;
        /// <summary>
        /// 配置名称,默认和表名一致,一般不会用到,方法以后扩展,如一个配置文件出现相同的表时,用来区分不同的配置
        /// </summary>
        public string SettingName
        {
            get
            {
                if (string.IsNullOrEmpty(_settingName))
                {
                    _settingName = Table;
                }
                return _settingName;
            }
            set
            {
                _settingName = value;
            }
        }
    
        #region 查询配置
        /// <summary>
        /// 查询的字段
        /// </summary>
        public string Select { get; set; }
        /// <summary>
        /// 查询的表名以及关联的表名,如 left join, right join
        /// </summary>
        public string From { get; set; }
        /// <summary>
        /// 查询的条件
        /// 前端返回的查询条件,只有出现在这些配置好的字段,才会生成为了 sql 的 where 条件,
        /// 没出现的字段会被忽略
        /// </summary>
        public Where Where { get; set; }
        /// <summary>
        /// 分页时必须会乃至的排序规则
        /// </summary>
        public OrderBy OrderBy { get; set; }
    
        public GroupBy GroupBy { get; set; }
        /// <summary>
        /// 页码
        /// </summary>
        public int PageNumber { get; set; }
        /// <summary>
        /// 页大小
        /// </summary>
        public int PageSize { get; set; }
    
    
        #endregion 查询配置
    
        /// <summary>
        /// 指定该配置所属于的表
        /// </summary>
        public string Table { get; set; }
    
        #region 增删改配置
        /// <summary>
        /// 对应前端返回的 json 格式数据的键名
        /// e.g.: {master:{inserted:[{data:{}}]}} 中的 master 就是这里要对应的 JsonName
        /// 注意默认主表的 jsonName 是 master, 所以主表一般可省略不写, 但子表必须得指定
        /// </summary>
        public string JsonName { get; set; }
        /// <summary>
        /// 自增的字段,指定了自增的字段,在 insert 时会自动忽略该字段
        /// </summary>
        public string ID { get; set; }
        /// <summary>
        /// 主键, 在保存成功后会返回主键的值; 
        /// </summary>
        public string PKs { get; set; }
        /// <summary>
        /// 唯一值的字段,对应数据库 unique, 在 insert,update 前会判断是否已存在
        /// </summary>
        public string Uniques { get; set; }
        /// <summary>
        /// 唯一值的字段的值是否允许为空
        /// </summary>
        public string UniqueAllowEmptys { get; set; }
        /// <summary>
        /// 所属的父级配置, 在 xml 中不用指定,程序会自动分析
        /// </summary>
        public SqlConfig Parent { get; set; }
        /// <summary>
        /// 包含的子级配置, 即子表的配置,需要在 xml 中配置
        /// </summary>
        public List<SqlConfig> Children { get; set; }
        /// <summary>
        /// 依赖父表的字段
        /// </summary>
        public Dependency Dependency { get; set; }
        /// <summary>
        /// insert 的配置
        /// </summary>
        public Insert Insert { get; set; }
        /// <summary>
        /// update 的配置
        /// </summary>
        public Update Update { get; set; }
        /// <summary>
        /// delete 的配置
        /// </summary>
        public Delete Delete { get; set; }
        #endregion
        /// <summary>
        /// 单条记录查询的配置,一般用在配置列表双击弹出那条记录的获取的 sql 
        /// </summary>
        public SingleQuery SingleQuery { get; set; }
        /// <summary>
        /// 导出配置
        /// </summary>
        public Export Export { get; set; }
        /// <summary>
        /// 导入配置
        /// </summary>
        public Import Import { get; set; }
        /// <summary>
        /// 是否物理删除?
        /// </summary>
        public bool DeleteAnyway { get; set; }
        /// <summary>
        /// 表单编码的生成配置
        /// </summary>
        public BillCodeRule BillCodeRule { get; set; }
    
    
    }
    
    5、批量增删改截图

    单表

    主-从

    主-从-从

    源码 https://github.com/grissomlau/Grissom.CMS

    初始化登录名:admin, 密码: 123
    

     
    分类: C#
  • 相关阅读:
    T450的Fn lock
    移民,不应该是走投无路后的选择
    门槛低的行业看天赋,门槛高的行业看毅力
    个人是时代的一朵浪花
    转载:XPath基本语法
    爪哇国新游记之三十四----Dom4j的XPath操作
    常去的论坛今天两个传统行业的坛友要下岗了
    异常中要了解的Throwable类中的几个方法
    感觉JVM的默认异常处理不够好,既然不好那我们就自己来处理异常呗!那么如何自己处理异常呢?
    JVM对异常的默认处理方案
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/6421813.html
Copyright © 2011-2022 走看看