編寫優雅程式碼,從挖掉噁心的if/else 開始

  • 2019 年 10 月 3 日
  • 筆記

背景

  長話短說, 作為開發人員經常需要根據條件靈活(過濾+排序)資料庫,不管你是用rawsql 還是EFCore, 以下類似偽程式碼大家都可能遇到:

        /// <summary>          /// 靈活過濾 能耗數據表  (rawsql)          /// </summary>          [Route("all")]          [HttpGet]          public async Task<List<CarEnergyModelEntity>> GetModeParametersAsync(             [FromQuery] string carVersion,             [FromQuery] string carId,             [FromQuery] string userId,             [FromQuery] string soVersion,             [FromQuery] string configVersion,             [FromQuery] string ConfigContent              )          {              StringBuilder strWhere = new StringBuilder(" 1=1 ");                if (!string.IsNullOrEmpty(carVersion))                  strWhere.Append($" and car_version='{carVersion}'");              if (!string.IsNullOrEmpty(carId))                  strWhere.Append($" and car_id_='{carId}'");              if (!string.IsNullOrEmpty(userId))                  strWhere.Append($" and user_id='{userId}'");              if (!string.IsNullOrEmpty(soVersion))                  strWhere.Append($" and so_version='{soVersion}'");              if (!string.IsNullOrEmpty(configVersion))                  strWhere.Append($" and config_version='{configVersion}'");              if (!string.IsNullOrEmpty(ConfigContent))                  strWhere.Append($" and config_content='{ConfigContent}'");                var dt = new DataTable();              using (SqlConnection con = new SqlConnection("//connectStr//"))              {                  var sql = $"select * from dbo.[car_energy_model] where {strWhere.ToString()}";                  using (SqlCommand cmd = new SqlCommand(sql, con))                  {                      // TODO                  }              }          }
       /// <summary>          /// 靈活過濾 能耗數據表  (EFCore)          /// </summary>          [Route("all")]          [HttpGet]          public async Task<List<CarEnergyModelEntity>> GetModeParametersAsync1(             [FromQuery] string carVersion,             [FromQuery] string carId,             [FromQuery] string userId,             [FromQuery] string soVersion,             [FromQuery] string configVersion,             [FromQuery] string ConfigContent              )          {              var sqlQuery = _context.CarEnergyModels;                if (!string.IsNullOrEmpty(carVersion))                  sqlQuery = sqlQuery.Where(x=>x.CarVersion == carVersion);              if (!string.IsNullOrEmpty(carId))                  sqlQuery = sqlQuery.Where(x => x.CarId == carId);              if (!string.IsNullOrEmpty(userId))                  sqlQuery = sqlQuery.Where(x => x.UserId == userId);              if (!string.IsNullOrEmpty(soVersion))                  sqlQUery = sqlQuery.Where(x => x.SoVersion == soVersion);              if (!string.IsNullOrEmpty(configVersion))                  sqlQuery = sqlQuery.Where(x => x.ConfigVersion == configVersion);              if (!string.IsNullOrEmpty(ConfigContent))                  sqlQuery = sqlQuery.Where(x => x.ConfigContent == ConfigContent);                return sqlQuery.ToList();          }

   特別是在大數據產品或者物聯網產品中,欄位甚多;需要 過濾/ 排序 的欄位千變萬化, if/else 寫到死,一邊寫一邊吐。

   寫出優雅漂亮的程式碼,從移除if/else 開始。

頭腦風暴

  從靈活查詢的要求看,每一個欄位都有為null 或 不為null 的可能, 以上偽程式碼6個欄位, 理論上僅過濾欄位最終執行查詢時形成的sql 共有2^6= 64種可能, 還不算 靈活的排序欄位。

現在我們要寫這麼多if 語法,是因為:

  -  在編碼階段,強制判斷欄位存在, 並據此組裝 rawsql

  -  在編碼階段,強制判斷欄位存在,並據此使用lambda強類型 構造IQueryable

為了解決這個痛點, 引入動態Linq,動態Linq的不同之處在於 查詢方法的參數不限於強類型的lamdba表達式,而是可以使用字元串;

使用字元串,意味著我們可在運行時動態決定過濾、排序內容

// 常規EF Linq: where條件過濾 + 倒排  _context.CarEnergyModels.Where(x=>x.CarVersion == carVersion).OrderByDescending(x=>x.UploadTime);    // 動態EF Linq: where 條件過濾 + 倒排  _context.CarEnergyModels.Where("carVersion=="ft_version_3.2"").OrderBy("UploadTime desc");

  同時由於我們在服務端可完全抓取QueryString(可一次性組裝動態Linq字元串), 故動態靈活構建查詢的方案呼之欲出。

編碼實踐

以上面偽程式碼業務舉例, 根據條件靈活查詢。

1.  nuget引入DynamicLinq:

Install-Package Microsoft.EntityFrameworkCore.DynamicLinq -Version 1.0.19

2. 定義EFCore 查詢實體類:

    public class CarModelContext : DbContext      {          public DbSet<CarEnergyModelEntity> CarEnergyModels { get; set; }            public CarModelContext(DbContextOptions<CarModelContext> options) : base(options)          {          }      }        [Table("car_energy_model")]      public class CarEnergyModelEntity      {          public CarEnergyModelEntity() { }            [JsonIgnore]          [Key]          public Guid Id { get; set; }            [Column("car_version")]          public string CarVersion { get; set; }          [Column("car_id")]          public string CarId { get; set; }            [Column("user_id")]          public string UserId { get; set; }            [Column("so_version")]          public string SoVersion { get; set; }            [Column("config_version")]          public string ConfigVersion { get; set; }            [Column("config_content")]          public string ConfigContent { get; set; }            [Column("uploadtime")]          public DateTime UploadTime => DateTime.UtcNow;      }

3. Query集合抓取所有QueryString,列舉欄位的方式 判斷欄位為null, 並構造查詢

        [Route("all")]          [HttpGet]          public async Task<List<CarEnergyModelEntity>> GetModeParametersAsync(             [FromQuery] string carVersion,             [FromQuery] string carId,             [FromQuery] string userId,             [FromQuery] string soVersion,             [FromQuery] string configVersion,             [FromQuery] string configContent              )          {         //   這裡使用列舉欄位的方式構造 strWhere              var query = HttpContext.Request.Query;              var validQueryArray1 = query.Where(x => (new string[] { "CarVersion", "carId", "userId", "soVersion", "configVersion", "configContent" }).Contains(x.Key, StringComparer.OrdinalIgnoreCase))                  .Where(x => !string.IsNullOrEmpty(x.Value))                  .Select(x => x.Key + "=="" + x.Value + """).ToArray();                string strWhere = string.Join(" and ", validQueryArray1);              strWhere = string.IsNullOrEmpty(strWhere) ? " 1=1" : strWhere;              var sqlQuery = _context.CarEnergyModels.Where(strWhere);                return sqlQuery.ToList();          }    

 EFCore生成的SQL如下:

SELECT [c].[Id], [c].[car_id], [c].[car_version], [c].[config_content], [c].[config_version], [c].[so_version], [c].[user_id]  FROM [car_energy_model] AS [c]  WHERE (((([c].[car_version] = N'FT_Version_3.2') AND ([c].[car_id] = N'CD292FE0900X')) AND ([c].[user_id] = N'u_1960988792x')) AND ([c].[so_version] = N'so_ver1.2')) AND ([c].[config_version] = N'cv_1.2') 

ok, That‘s all 

以上查詢還可擴展:前端組裝排序字元串(orderStr:Uploadtime descending)通過QueryString傳給API,API通過DyanmicLinq構造靈活的排序欄位。

經過驗證,以上過濾和排序都是在SqlServer層面完成的。

移除噁心的 if、else之後程式碼是不是看起來更優雅一些。

總結

以上場景相信很多開發者都會遇到,特別是進階到一定水平,移除if/else  的慾望愈加強烈。

再次強化本文 知識點:   

  DynamicLinq 具備動態形成查詢條件的能力,不再依靠lambda 強類型表達式,而是根據構造的過濾和排序字元串,內部解析成查詢條件。

 

--------------------2019/9/23 下班前更新--------------------------------------

DynamicLinq  若動態組裝String,確實存在 SQL注入問題, 使用placeholder 可避免。

更新程式碼:

            // 構建動態查詢              var query = HttpContext.Request.Query;              var validQueryArray1 = query.Where(x => (new string[] { "CarVersion", "carId", "userId", "soVersion", "configVersion", "configContent" }).Contains(x.Key, StringComparer.OrdinalIgnoreCase))                  .Where(x => !string.IsNullOrEmpty(x.Value));                var predicate = validQueryArray1.Select((x,i) => $"{x.Key}==@{i}").ToArray();              var paramses = validQueryArray1.Select(x=>x.Value.ToString()).ToArray();              string strPredicate = string.Join(" and ", predicate);              strPredicate = string.IsNullOrEmpty(strPredicate) ? " 1=1" : strPredicate;              var sqlQuery = _context.CarEnergyModels.Where(strPredicate, paramses);                return sqlQuery.ToList();

Tags: