企業級自定義表單引擎解決方案(三)--實體物件模型實現
實體物件模型與資料庫對應實現
主要是解決實體物件模型與資料庫之間的一一對應,在介面上新增實體物件模型,增加欄位,則同步管理業務實體資料庫表結構,主要的思路就是介面上修改了實體模型,同步執行修改資料庫表結構的Sql語句(已經運行了一段時間的業務表,需要DBA實現修改資料庫再修改實體模型),介面大概如下:
核心程式碼:
定義抽象類AutoBusinessDbServiceBase,介面增刪改實體物件模型之後,同步執行Sql語句修改不同資料庫的修改資料庫表結構的Sql語句,定義抽象類遮蔽不同資料庫之間的語句區別。
public abstract class AutoBusinessDbServiceBase : IAutoBusinessDbService { protected IUnitOfWork _unitOfWork; public AutoBusinessDbServiceBase(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } public async Task<bool> CreateTable(SpriteObjectDto spriteObjectDto) { if (CheckTableExists(spriteObjectDto.Name)) { throw new SpriteException("資料庫表已經存在,請聯絡管理員!"); } await DoCreateTable(spriteObjectDto); return await Task.FromResult(true); } /// <summary> /// 判斷資料庫表是否存在 /// </summary> protected abstract bool CheckTableExists(string tableName); /// <summary> /// 執行建立表過程 /// </summary> /// <param name="spriteObjectDto"></param> /// <returns></returns> protected abstract Task<bool> DoCreateTable(SpriteObjectDto spriteObjectDto); public abstract Task<bool> AddObjectProperty(ObjectProperty objectProperty, string tableName); public abstract Task<bool> ModifyObjectProperty(ObjectProperty objectProperty, string tableName); public abstract Task<bool> DeleteObjectProperty(string propertyName, string tableName); }
下面是Mysql資料庫的實現,程式碼比較簡單,節約篇幅,不貼程式碼了,程式碼地址:https://gitee.com/kuangqifu/sprite/blob/master/03_form/CK.Sprite.Form/CK.Sprite.Form.MySql/Domain/DesignTime/MysqlAutoBusinessDb.cs
執行時JObject程式設計
Newtonsoft.Json,對於這個元件應該不會陌生,用得比較多的是Json序列化與反序列化,他的核心是圍繞JToken來實現的,他提供了對於Json物件的動態程式設計能力(當然還有其他的元件,但用得廣泛的還是這個元件),對於自定義表單的實現,這個就尤其重要了,前端建立物件、編輯物件、查詢引數等,都是以Json物件格式儲存的,執行時,動態解析Json物件,拼接返回結果並返回給前端使用,都是圍繞著動態Json程式設計實現的。
執行時預設常規方法實現
常規增刪改查等Sql方法執行,完全可以內建實現,這裡採用Dapper來實現的,開源專案實現了Mysql資料庫的實現,參考地址:https://gitee.com/kuangqifu/sprite/blob/master/03_form/CK.Sprite.Form/CK.Sprite.Form.MySql/Repository/MysqlRuntimeRepository.cs,重點介紹部分方法:
新增業務實體
前端介面根據規則引擎獲取使用者新增的Json實體物件,最終會呼叫預設的建立資料庫業務資料的方法,方法內部會根據之前文章介紹的SpriteObject物件進行資料過濾,並自動生成不同型別的Id欄位值,動態新增新增審計日誌,如果是樹形結構,還會動態維護PId,Code等欄位值,呼叫完成之後,並返回新建立的Id值,程式碼如下:
public async Task<JObject> DoDefaultCreateMethodAsync(SpriteObjectDto spriteObjectDto, JObject paramValues, string sqlMethodContent = "") { StringBuilder sbInsertFields = new StringBuilder(); StringBuilder sbInsertValues = new StringBuilder(); var newGuidId = Guid.NewGuid(); if (spriteObjectDto.KeyType == EKeyType.Guid) { sbInsertFields.Append($"{MysqlConsts.PreMark}Id{MysqlConsts.PostMark},"); sbInsertValues.Append($"'{newGuidId}',"); } else { sbInsertFields.Append($"{MysqlConsts.PreMark}Id{MysqlConsts.PostMark},"); sbInsertValues.Append($"0,"); } foreach (var paramValue in paramValues) { var field = paramValue.Key; var findProperty = spriteObjectDto.ObjectPropertyDtos.FirstOrDefault(r => r.Name.ToLower() == field.ToLower()); if (findProperty != null) { if (findProperty.FieldType != EFieldType.String && findProperty.FieldType != EFieldType.Text) { if (string.IsNullOrEmpty(paramValue.Value.ToString())) { paramValues[field] = null; } } sbInsertFields.Append($"{MysqlConsts.PreMark}{field}{MysqlConsts.PostMark},"); sbInsertValues.Append($"@{field},"); } } var tempParamValues = paramValues.DeepClone().ToObject<JObject>(); var nowTime = DateTime.Now; if (spriteObjectDto.IsTree) { CreateTree(sbInsertFields, sbInsertValues, spriteObjectDto, tempParamValues); } if (spriteObjectDto.CreateAudit) { CreateAuditCreate(sbInsertFields, sbInsertValues, nowTime, tempParamValues); } if (spriteObjectDto.ModifyAudit) { CreateAuditUpdate(sbInsertFields, sbInsertValues, nowTime, tempParamValues); } var strInserSql = (string.IsNullOrEmpty(sqlMethodContent) ? SqlDefaultCreate : sqlMethodContent) .Replace("#TableName#", spriteObjectDto.Name) .Replace("#Fields#", sbInsertFields.ToString().TrimEnd(',')) .Replace("#Values#", sbInsertValues.ToString().TrimEnd(',')); JObject result = new JObject(); if (spriteObjectDto.KeyType == EKeyType.Guid) { await _unitOfWork.Connection.ExecuteAsync(strInserSql, tempParamValues.ToConventionalDotNetObject()); result.Add(new JProperty("result", newGuidId)); } else { var resultId = await _unitOfWork.Connection.QueryFirstAsync<int>(strInserSql + "SELECT LAST_INSERT_ID();", tempParamValues.ToConventionalDotNetObject()); result.Add(new JProperty("result", resultId)); } return result; }
其他幾種預設實現不單獨介紹了,實現比較類似,可以直接閱讀原始碼。另外介紹一下動態Where語句的實現。
Where語句可能會非常的複雜,很多時候直接寫Sql語句的Where方法就很麻煩了,如果要讓自定義表單自動完成Sql語句的封裝,則需要一種不同的資料結構才能實現。動態Where的模型採用樹結構實現,稱為Sql表示式樹,表示式列舉有三種,And、Or、Condition,核心還是根據Sql表示式樹生成Where後面的Sql語句,並拼接Dapper執行引數。
模型定義:
public class ExpressSqlModel { public ESqlExpressType SqlExpressType { get; set; } public string Field { get; set; } public EConditionType ConditionType { get; set; } public object Value { get; set; } public List<ExpressSqlModel> Children { get; set; } } public class QueryWhereModel { /// <summary> /// 查詢欄位名稱 /// </summary> public string Field { get; set; } /// <summary> /// 等於 = 1,不等於 = 2,Between = 3,In = 4,Like = 5,大於 = 6,大於等於 = 7,小於 = 8,小於等於 = 9,Null = 10,NotNull = 11,NotIn = 12 /// </summary> public EConditionType ConditionType { get; set; } /// <summary> /// **傳遞集合時,直接傳遞陣列** /// </summary> public object Value { get; set; } } /// <summary> /// Sql 表示式樹 /// </summary> public enum ESqlExpressType { And = 1, Or = 2, Condition = 3 }
表示式核心方法:
public delegate string CreateSqlWhereDelegate(JObject sqlWhereParamValues, ExpressSqlModel expressSqlModel, ref int index); public class ExpressSqlHelper { public static string CreateSqlWhere(ExpressSqlModel expressSqlModel, JObject sqlWhereParamValues, CreateSqlWhereDelegate createSqlWhereDelegate) { var sqlIndex = 1; if (expressSqlModel.SqlExpressType == ESqlExpressType.Condition) { return createSqlWhereDelegate(sqlWhereParamValues, expressSqlModel, ref sqlIndex); } else { return $"({CreateComplexSql(expressSqlModel, sqlWhereParamValues, ref sqlIndex, createSqlWhereDelegate)})"; } } private static string CreateComplexSql(ExpressSqlModel expressSqlModel, JObject sqlWhereParamValues,ref int sqlIndex, CreateSqlWhereDelegate createSqlWhereDelegate) { string strResutl = ""; string endCondition = ""; if (expressSqlModel.SqlExpressType == ESqlExpressType.And) { endCondition = "AND"; } else { endCondition = "OR"; } int index = 1; foreach (var childExpress in expressSqlModel.Children) { string tempCondition = index == expressSqlModel.Children.Count ? "" : $" {endCondition} "; if (childExpress.SqlExpressType == ESqlExpressType.Condition) { if(childExpress.Value != null) { strResutl += $"{createSqlWhereDelegate(sqlWhereParamValues, childExpress, ref sqlIndex)}{ tempCondition }"; } } else { strResutl += $"({CreateComplexSql(childExpress, sqlWhereParamValues, ref sqlIndex, createSqlWhereDelegate)}){tempCondition}"; } index++; } return strResutl; } public static string TestCreateConditionSql(JObject sqlWhereParamValues, ExpressSqlModel expressSqlModel, ref int index) { string preMark = "`"; string postMark = "`"; var conditionType = expressSqlModel.ConditionType; var field = expressSqlModel.Field; StringBuilder sbSqlWhere = new StringBuilder(); switch (conditionType) { case EConditionType.等於: sbSqlWhere.Append($"{preMark}{field}{postMark}=@SW{index}_{field}"); sqlWhereParamValues.Add(new JProperty($"SW{index}_{field}", expressSqlModel.Value)); break; case EConditionType.Like: sbSqlWhere.Append($"{preMark}{field}{postMark} LIKE CONCAT('%',@SW{index}_{field},'%')"); sqlWhereParamValues.Add(new JProperty($"SW{index}_{field}", expressSqlModel.Value)); break; case EConditionType.In: sbSqlWhere.Append($"{preMark}{field}{postMark} IN @SW{index}_{field}"); sqlWhereParamValues.Add(new JProperty($"SW{index}_{field}", expressSqlModel.Value)); break; case EConditionType.Between: sbSqlWhere.Append($"{preMark}{field}{postMark} BETWEEN @SW{index}_{field}_1 AND @SW{index}_{field}_2"); var inValues = expressSqlModel.Value as ArrayList; sqlWhereParamValues.Add(new JProperty($"SW{index}_{field}_1", inValues[0])); sqlWhereParamValues.Add(new JProperty($"SW{index}_{field}_2", inValues[1])); break; case EConditionType.大於: sbSqlWhere.Append($"{preMark}{field}{postMark}>@SW{index}_{field}"); sqlWhereParamValues.Add(new JProperty($"SW{index}_{field}", expressSqlModel.Value)); break; case EConditionType.大於等於: sbSqlWhere.Append($"{preMark}{field}{postMark}>=@SW{index}_{field}"); sqlWhereParamValues.Add(new JProperty($"SW{index}_{field}", expressSqlModel.Value)); break; case EConditionType.小於: sbSqlWhere.Append($"{preMark}{field}{postMark}<@SW{index}_{field}"); sqlWhereParamValues.Add(new JProperty($"SW{index}_{field}", expressSqlModel.Value)); break; case EConditionType.小於等於: sbSqlWhere.Append($"{preMark}{field}{postMark}<=@SW{index}_{field}"); sqlWhereParamValues.Add(new JProperty($"SW{index}_{field}", expressSqlModel.Value)); break; case EConditionType.不等於: sbSqlWhere.Append($"{preMark}{field}{postMark}<>@SW{index}_{field}"); sqlWhereParamValues.Add(new JProperty($"SW{index}_{field}", expressSqlModel.Value)); break; case EConditionType.Null: sbSqlWhere.Append($"{preMark}{field}{postMark} IS NULL"); break; case EConditionType.NotNull: sbSqlWhere.Append($"{preMark}{field}{postMark} IS NOT NULL"); break; case EConditionType.NotIn: sbSqlWhere.Append($"{preMark}{field}{postMark} NOT IN @SW{index}_{field}"); sqlWhereParamValues.Add(new JProperty($"SW{index}_{field}", expressSqlModel.Value)); break; default: break; } index++; return sbSqlWhere.ToString(); } public static JsonSerializer CreateCamelCaseJsonSerializer() { return new JsonSerializer { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }; } }
執行時特殊方法執行實現
常規Sql方法不能滿足所有的需求,對於複雜的語句,提供了自定義的功能,主要是自定義Sql執行,反射執行自定義新增的方法(還可執行自定義Rpc的呼叫)。程式碼不一一介紹了,參考:https://gitee.com/kuangqifu/sprite/blob/master/03_form/CK.Sprite.Form/CK.Sprite.Form.Core/Domain/RunTime/RuntimeService.cs
這篇文章介紹了自定義表單執行時方法的執行設計實現,有些設計思想還是可以拆分出來應用到我們現有的系統中,比如我們要實現動態Sql語句查詢,則完全可以實現動態Where部分邏輯,由頁面使用者選擇需要哪些查詢欄位和查詢條件(比如=、!=、IN、Like等),我們可以動態生成Sql where表示式。這部分內容對於自定義表單實現,還是比較重要的,建議可以閱讀原始碼。
&n