背景

  長話短說, 作為開發人員經常需要根據條件靈活查詢數據庫,不管你是用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 

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

總結

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

再次強化本文 知識點:   

  DynamicLinq 具備動态形成查詢條件的能力,不再依靠lambda 強類型表達式,而是根據構造的查詢字符串,内部解析成查詢條件。

 

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

DynamicLinq  若動态組裝String,确實存在 SQL注入問題, 使用placeholder 可避免
<https://stackoverflow.com/questions/8738953/is-injection-possible-through-dynamic-linq>


更新代碼:
// 構建動态查詢 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}[email protected]{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();