优雅地构建动态、复杂且安全的 SQL 查询

优雅地构建动态、复杂且安全的 SQL 查询

在现代应用程序开发中,我们经常需要根据用户输入或复杂的业务规则来动态构建 SQL 查询。一个常见挑战是如何优雅且安全地处理这些多变的查询条件。简单地拼接字符串不仅容易出错,还会带来严重的安全漏洞,如 SQL 注入。

本文将以一个设计精良的 SearchHelper.cs 工具类为例,深入探讨一种在 C# 中构建动态、可嵌套的复杂查询条件的强大方法。我们将学习如何设计可复用的查询模型、如何通过递归生成 SQL WHERE 子句,以及如何确保查询的安全性。

1. 问题的根源:为什么简单的字符串拼接是危险的?

让我们从一个反面教材开始。假设我们需要根据用户输入的姓名和年龄进行查询,很多人可能会这样写:

csharp 复制代码
string name = userNameInput.Text;
string age = ageInput.Text;

string sql = "SELECT * FROM Users WHERE 1=1";

if (!string.IsNullOrEmpty(name))
{
    sql += " AND Name = '" + name + "'";
}
if (!string.IsNullOrEmpty(age))
{
    sql += " AND Age > " + age;
}

// 执行 SQL...

这种方式存在两个致命问题:

  1. SQL 注入风险 : 如果用户在 name 输入框中输入 '; DROP TABLE Users;--,拼接后的 SQL 就会变成一场灾难。
  2. 代码难以维护 : 随着查询条件越来越复杂(例如,需要支持 ORINBETWEEN 等),if-else 逻辑会迅速膨胀,变得混乱不堪。

2. 核心设计:将查询条件模型化

要优雅地解决这个问题,第一步是将查询条件抽象成一个数据模型。让我们定义一个 ComplexQuery 类,它可以像树一样表示复杂的逻辑关系。

csharp 复制代码
public class ComplexQuery
{
    /// <summary>
    /// 逻辑操作符 (AND / OR)
    /// </summary>
    public string condition { get; set; } = "AND";

    /// <summary>
    /// 嵌套的规则列表
    /// </summary>
    public List<ComplexQuery> rules { get; set; } = new List<ComplexQuery>();

    /// <summary>
    /// 查询字段
    /// </summary>
    public string field { get; set; }

    /// <summary>
    /// 查询操作符 (如 equal, contains, in 等)
    /// </summary>
    public string @operator { get; set; }

    /// <summary>
    /// 查询值
    /// </summary>
    public List<string> values { get; set; } = new List<string>();

    /// <summary>
    /// 字段类型 (如 datetime)
    /// </summary>
    public string @type { get; set; }
}

这个设计的精妙之处在于 rules 属性,它允许一个 ComplexQuery 对象包含多个子 ComplexQuery 对象,从而无限嵌套,形成复杂的查询树。

例如,要表示 (Age > 30 AND (Department = 'Sales' OR Department = 'Marketing')),我们可以这样构建对象:

csharp 复制代码
var query = new ComplexQuery
{
    condition = "AND",
    rules = new List<ComplexQuery>
    {
        new ComplexQuery { field = "Age", @operator = "greater", values = new List<string> { "30" } },
        new ComplexQuery
        {
            condition = "OR",
            rules = new List<ComplexQuery>
            {
                new ComplexQuery { field = "Department", @operator = "equal", values = new List<string> { "Sales" } },
                new ComplexQuery { field = "Department", @operator = "equal", values = new List<string> { "Marketing" } }
            }
        }
    }
};

3. 递归生成 SQL WHERE 子句

有了查询模型,我们就可以编写一个递归函数来解析这个树状结构,并生成对应的 SQL WHERE 子句。这正是 SearchHelper.csSearchInfoToWhere 方法的核心思想。

csharp 复制代码
public static class SearchHelper
{
    // ... 操作符常量定义 ...
    public const string EQUAL = "equal";
    public const string CONTAINS = "contains";
    // ... 其他操作符

    public static string SearchInfoToWhere<T>(ComplexQuery complexQuery, ref List<object> 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<T>(rule, ref parameters))
                .Where(clause => !string.IsNullOrEmpty(clause))
                .ToList();

            if (subClauses.Count == 0)
            {
                return "";
            }

            return $"({string.Join($" {complexQuery.condition} ", subClauses)})";
        }
        // 否则,处理单个规则
        else
        {
            // 根据 operator 生成单个条件语句
            // 使用参数化查询来防止 SQL 注入
            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:
                    // IN 的处理需要生成多个参数
                    var inParams = new List<string>();
                    foreach(var val in complexQuery.values)
                    {
                        var pName = $"@{parameters.Count}";
                        parameters.Add(val);
                        inParams.Add(pName);
                    }
                    return $"{complexQuery.field} IN ({string.Join(", ", inParams)})";
                // ... 其他 case ...
                default:
                    return "";
            }
        }
    }
}

注意:上面的代码是根据 SearchHelper.cs 核心思想简化的示例,用于说明原理。

关键点:

  1. 递归调用 : 当一个 ComplexQuery 节点包含 rules 时,函数会遍历这些 rules 并对每个 rule 进行递归调用。
  2. 参数化查询 : 这是安全性的基石。不直接将值拼接到 SQL 语句中,而是使用占位符(如 @0, @1),然后将实际值添加到一个单独的参数列表中。这从根本上杜绝了 SQL 注入的可能。
  3. 操作符处理 : switch 语句根据不同的 @operator 生成不同的 SQL 语法,例如 LIKE 用于 CONTAINSIN (...) 用于 IN 操作。

4. 实践应用

现在,可以轻松地使用它来构建查询了。

csharp 复制代码
// 1. 构建复杂的查询模型
var queryModel = new ComplexQuery { /* ... 如上文示例 ... */ };

// 2. 初始化参数列表
var parameters = new List<object>();

// 3. 生成 WHERE 子句
string whereClause = SearchHelper.SearchInfoToWhere<User>(queryModel, ref parameters);

// 4. 构建最终 SQL
string finalSql = $"SELECT * FROM Users WHERE {whereClause}";

// 5. 使用 Dapper、EF Core 或 ADO.NET 执行查询
// 例如,使用 Dapper:
// var users = connection.Query<User>(finalSql, new DynamicParameters(parameters));

5. 进阶应用:支持用户自定义表单的动态查询

这套设计模式最强大的应用之一,便是为用户自定义表单(或称动态表单、低代码平台数据源)提供后端查询能力。在这类场景下,数据表的字段是动态变化的,我们无法在编译时为每个表都创建一个对应的 C# 实体类。

解决方案的核心在于:前端负责根据用户的筛选条件构建 ComplexQuery 对象的 JSON 结构,而后端则通用地解析这个结构来生成 SQL。由于 field 本身就是字符串,后端无需提前知道具体的字段名。SearchHelper.cs 中对 Dictionary<string, object> 的处理能力,正是为此类场景量身打造。

工作流程示例:

  1. 前端生成查询 JSON:用户在筛选器界面操作后,前端生成如下 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"
        }
      ]
    }
  2. 后端通用接口处理

    csharp 复制代码
    // 1. 从前端接收 JSON
    string jsonQuery = GetQueryJsonFromRequest();
    
    // 2. 反序列化为 ComplexQuery 对象
    var queryModel = JsonConvert.DeserializeObject<ComplexQuery>(jsonQuery);
    
    // 3. 初始化参数列表
    var parameters = new List<object>();
    
    // 4. 调用一个非泛型或面向 Dictionary 的版本来生成 WHERE 子句
    //    这里的 SearchInfoToWhere 不再需要泛型约束 <T>,可以直接处理字符串字段
    string whereClause = SearchHelper.SearchInfoToWhere(queryModel, ref parameters);
    
    // 5. 构建并执行 SQL
    string tableName = "UserDefinedTable_123"; // 表名也可以是动态的
    string finalSql = $"SELECT * FROM {tableName} WHERE {whereClause}";
    
    // 6. 执行查询...

通过这种方式,后端查询接口变得高度通用,能够为任意结构的自定义表单提供服务,而无需修改一行后端代码,真正实现了数据结构与查询逻辑的解耦。

7. 结论

通过将查询条件模型化并使用递归来解析模型,可以构建一个强大、灵活且安全的动态 SQL 查询生成器。这种方法不仅解决了 SQL 注入的风险,还极大地提高了代码的可读性和可维护性,使开发者能够从容应对各种复杂的查询需求。

SearchHelper.cs 的实现展示了一个优秀的设计模式。其核心思想------将查询条件模型化并利用递归进行解析------是解决动态查询问题的关键。通过理解并应用这种模式,开发者可以在自己的项目中构建出同样优雅且安全的查询解决方案。

相关推荐
滴水寸金2 小时前
讯飞语音转文本:定位阅读进度与高亮文本的技术实现
后端
karry_k2 小时前
Java的类加载器
后端
ZZHHWW2 小时前
高性能架构01 -- 开篇
后端·架构
程序员小潘3 小时前
Spring Gateway动态路由实现方案
后端·spring cloud
golang学习记3 小时前
国内完美安装 Rust 环境 + VSCode 编写 Hello World 完整指南(2025 最新)
后端
Undoom3 小时前
解锁超级生产力:手把手教你构建与GitHub深度集成的自动化工作流,让AI成为你的编程副驾驶
后端
我是华为OD~HR~栗栗呀3 小时前
前端面经-高级开发(华为od)
java·前端·后端·python·华为od·华为·面试
菜鸟小九4 小时前
SSM(MybatisPlus)
java·开发语言·spring boot·后端
不爱编程的小九九4 小时前
小九源码-springboot051-智能推荐旅游平台
java·spring boot·后端