【開源.NET】 分享一個前後端分離的輕量級內容管理框架(第二篇前後端交互數據結構分析)

這是 CMS 框架系列文章的第二篇,第一篇開源了該框架的代碼和簡要介紹了框架的目的、作用和思想,這篇主要解析如何把sql 轉成標準 xml 配置文件和把前端post的增刪改數據規範成方便後臺解析的結構,以實現後端自動化操作數據庫。

【開源.NET】 分享一個前後端分離的輕量級內容管理框架(第一篇)

信息管理系統

信息管理系統關鍵功能:列表分頁和搜索、方便數據展示和錄入。業務複雜度通常在於多表關聯的搜索、錄入以及表與表和字段與字段之間的約束,還有就是報表統計了。除去報表不說,其它功能其實就是對數據庫表進行增刪改查,它們是獨立於業務存在的,所以可對它們進行規範化和自動化。
信息管理系統就是爲了方便數據的展示和錄入,簡化爲 “需求數據 - SQL - 數據庫”, SQL 作爲“需求數據” 與“數據庫”的中介。想要自動化增刪改查,必須要規範化“需求數據的結構”以及添加規範化的“配置文件”,用程序對它們進行分析以生成中介“SQL”。

一、自動化搜索和分頁

1、設計圖

488490-20170214181411816-1546454088.png

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

488490-20170214181443363-961478235.png

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

488490-20170214181456754-1403722098.png

二、自動化增刪改

1、設計圖

488490-20170214181508441-1045954171.png

將前端返回合法的 json 數據結合後臺配置好的xml, 經由 EasyCore 解析生成標準的 sql。

2、單表

假設有張視頻表,後臺管理員可對它進行增刪改操作。

488490-20170214181525285-485410226.png

在界面“新增或編輯”,點“保存”後,將數據組織成 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、主從表

假設用戶可對視頻單獨進行“評論”或“評分”, 還可以對“評論”進行“頂、踩”,而後臺管理員可對它們進行增刪改操作,簡化的表結構如下:

488490-20170214181553519-1274733873.png

這就是一個典型的“主 - 從 - 從”的表結構,這裏比單表多了一從信息:子表,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 結構難以看出它的規律,把它換成腦圖:

488490-20170214181608832-1861957195.png

其實就是一顆樹,提取它的結構:

488490-20170214181618347-1609136316.png

從圖可看出,樹的差異結構最深層級爲 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>
              <Field Name="CommentId" 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>
      </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、批量增刪改截圖

單表

488490-20170214181643472-1075315661.png

主-從

488490-20170214181657535-70884913.png

主-從-從

488490-20170214181708941-2103702072.png

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

初始化登錄名:admin, 密碼: 123

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章