前后端分离的轻量级内容管理框架
2017-02-20 22:10
639 查看
分享一个前后端分离的轻量级内容管理框架(第二篇前后端交互数据结构分析)
这是 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#
相关文章推荐
- 分享一个前后端分离的轻量级内容管理框架
- 【开源.NET】 分享一个前后端分离的轻量级内容管理框架(第二篇前后端交互数据结构分析)
- 【开源.NET】 分享一个前后端分离的轻量级内容管理框架(第二篇前后端交互数据结构分析)
- 【开源.NET】 分享一个前后端分离的轻量级内容管理框架
- 【开源.NET】 分享一个前后端分离的轻量级内容管理框架(第二篇前后端交互数据结构分析)
- 分享一个前后端分离的轻量级内容管理框架
- 【开源.NET】 分享一个前后端分离的轻量级内容管理框架
- 【开源.NET】 分享一个前后端分离的轻量级内容管理框架(第二篇前后端交互数据结构分析)
- 【开源.NET】 分享一个前后端分离的轻量级内容管理框架
- 【开源.NET】 轻量级内容管理框架Grissom.CMS(第二篇前后端交互数据结构分析)
- 【开源.NET】轻量级内容管理框架Grissom.CMS(第三篇解析配置文件和数据以转换成 sql)
- Nodeway--基于node.js的轻量级前后端分离框架
- 基于NodeJS的前后端分离的思考与实践(三)轻量级的接口配置建模框架
- 在前后端分离的项目中,后台使用shiro框架时,怎样使用它的会话管理系统(session),从而实现权限控制
- 【开源.NET】轻量级内容管理框架Grissom.CMS(第三篇解析配置文件和数据以转换成 sql)
- 基于NodeJS的前后端分离的思考与实践(三)轻量级的接口配置建模框架
- 从壹开始前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之七 || API项目整体搭建 6.2 轻量级ORM
- 【开源.NET】轻量级内容管理框架Grissom.CMS(第三篇解析配置文件和数据以转换成 sql)
- Nodeway--基于node.js的轻量级前后端分离框架
- 中后端管理系统前后分离、前端框架的实现拙见