摘要:在现代应用程序开发中,我们经常需要根据用户输入或复杂的业务规则来动态构建 SQL 查询。一个常见挑战是如何优雅且安全地处理这些多变的查询条件。简单地拼接字符串不仅容易出错,还会带来严重的安全漏洞,如 SQL 注入。
在现代应用程序开发中,我们经常需要根据用户输入或复杂的业务规则来动态构建 SQL 查询。一个常见挑战是如何优雅且安全地处理这些多变的查询条件。简单地拼接字符串不仅容易出错,还会带来严重的安全漏洞,如 SQL 注入。
本文将以一个设计精良的 SearchHelper.cs 工具类为例,深入探讨一种在 C# 中构建动态、可嵌套的复杂查询条件的强大方法。我们将学习如何设计可复用的查询模型、如何通过递归生成 SQL WHERE 子句,以及如何确保查询的安全性。
让我们从一个反面教材开始。假设我们需要根据用户输入的姓名和年龄进行查询,很多人可能会这样写: #技术分享
string name = userNameInput.Text;string age = ageInput.Text;string sql = "SELECT * FROM Users WHERE 1=1";if (!string.IsnullOrEmpty(name)) { sql += " AND Name = '" +} if (!string.IsNullOrEmpty(age)) { SQL += " AND Age > " +}这种方式存在两个致命问题:
SQL 注入风险 : 如果用户在 name 输入框中输入 '; DROP TABLE Users;-- ,拼接后的 SQL 就会变成一场灾难。代码难以维护 : 随着查询条件越来越复杂(例如,需要支持 OR 、 IN 、 BETWEEN 等), if-else 逻辑会迅速膨胀,变得混乱不堪。要优雅地解决这个问题,第一步是将查询条件抽象成一个数据模型。让我们定义一个 ComplexQuery 类,它可以像树一样表示复杂的逻辑关系。
public class ComplexQuery{ public string condition { get; set; } = "AND";public List rules { get; set; } = new List;public string field { get; set; }public string @operator { get; set; }public List values { get; set; } = new List;public string @type { get; set; } }这个设计的精妙之处在于 rules 属性,它允许一个 ComplexQuery 对象包含多个子 ComplexQuery 对象,从而无限嵌套,形成复杂的查询树。
例如,要表示 (Age > 30 AND (Department = 'Sales' OR Department = 'Marketing')) ,我们可以这样构建对象:
var query = new ComplexQuery{ condition = "AND", rules = new List{ new ComplexQuery { field = "Age", @operator = "greater", values = new List { "30" } }, new ComplexQuery { condition = "OR", rules = new List{ new ComplexQuery { field = "Department", @operator = "equal", values = new List { "Sales" } }, new ComplexQuery { field = "Department", @operator = "equal", values = new List { "Marketing" } } } } } };有了查询模型,我们就可以编写一个递归函数来解析这个树状结构,并生成对应的 SQL WHERE 子句。这正是 SearchHelper.cs 中 SearchInfoToWhere 方法的核心思想。
public static class SearchHelper{ public const string EQUAL = "equal"; public const string CONTAINS = "contains";public static string SearchInfoToWhere(ComplexQuery complexQuery, ref List parameters) { if (complexQuery == null || (complexQuery.rules == null || complexQuery.rules.Count == 0) && string.IsNullOrEmpty(complexQuery.field)) { return ""; }if (complexQuery.rules != null && complexQuery.rules.Count > 0) { var subClauses = complexQuery.rules .Select(rule => SearchInfoToWhere(rule, ref parameters)) .Where(clause => !string.IsNullOrEmpty(clause)) .ToList;if (subClauses.Count == 0) { return ""; }return $"({string.Join($" {complexQuery.condition} ", subClauses)})"; } else { var paramName = $"@{parameters.Count}"; parameters.Add(complexQuery.values.FirstOrDefault);switch (complexQuery.@operator) { case EQUAL: return $"{complexQuery.field} = {paramName}"; case CONTAINS: parameters[parameters.Count - 1] = $"%{complexQuery.values.FirstOrDefault}%"; return $"{complexQuery.field} LIKE {paramName}"; case IN: var inParams = new List; foreach(var val in complexQuery.values) { var pName = $"@{parameters.Count}"; parameters.Add(val); inParams.Add(pName); } return $"{complexQuery.field} IN ({string.Join(", ", inParams)})"; default: return ""; } } } }注意:上面的代码是根据 SearchHelper.cs 核心思想简化的示例,用于说明原理。
递归调用 : 当一个 ComplexQuery 节点包含 rules 时,函数会遍历这些 rules 并对每个 rule 进行递归调用。参数化查询 : 这是安全性的基石。不直接将值拼接到 SQL 语句中,而是使用占位符(如 @0 , @1 ),然后将实际值添加到一个单独的参数列表中。这从根本上杜绝了 SQL 注入的可能。操作符处理 : switch 语句根据不同的 @operator 生成不同的 SQL 语法,例如 LIKE 用于 CONTAINS , IN (...) 用于 IN 操作。现在,可以轻松地使用它来构建查询了。
var queryModel = new ComplexQuery { };var parameters = new List;string whereClause = SearchHelper.SearchInfoToWhere(queryModel, ref parameters);string finalSql = $"SELECT * FROM Users WHERE {whereClause}";这套设计模式最强大的应用之一,便是为用户自定义表单(或称动态表单、低代码平台数据源)提供后端查询能力。在这类场景下,数据表的字段是动态变化的,我们无法在编译时为每个表都创建一个对应的 C# 实体类。
解决方案的核心在于:前端负责根据用户的筛选条件构建 ComplexQuery 对象的 JSON 结构,而后端则通用地解析这个结构来生成 SQL。由于 field 本身就是字符串,后端无需提前知道具体的字段名。SearchHelper.cs 中对 Dictionary 的处理能力,正是为此类场景量身打造。
工作流程示例:
前端生成查询 JSON :用户在筛选器界面操作后,前端生成如下 JSON 字符串。{ "condition": "AND", "rules": [ { "field": "custom_field_01", "operator": "contains", "values": ["some_value"] }, { "field": "user_defined_date", "operator": "greater", "values": ["2023-01-01"], "type": "datetime" } ]}后端通用接口处理string jsonQuery = GetQueryJsonFromRequest;var queryModel = JsonConvert.DeserializeObject(jsonQuery);var parameters = new List;string whereClause = SearchHelper.SearchInfoToWhere(queryModel, ref parameters);string tableName = "UserDefinedTable_123"; string finalSql = $"SELECT * FROM {tableName} WHERE {whereClause}";通过这种方式,后端查询接口变得高度通用,能够为任意结构的自定义表单提供服务,而无需修改一行后端代码,真正实现了数据结构与查询逻辑的解耦。
通过将查询条件模型化并使用递归来解析模型,可以构建一个强大、灵活且安全的动态 SQL 查询生成器。这种方法不仅解决了 SQL 注入的风险,还极大地提高了代码的可读性和可维护性,使开发者能够从容应对各种复杂的查询需求。
SearchHelper.cs 的实现展示了一个优秀的设计模式。其核心思想——将查询条件模型化并利用递归进行解析——是解决动态查询问题的关键。通过理解并应用这种模式,开发者可以在自己的项目中构建出同样优雅且安全的查询解决方案。
来源:墨码行者