数据库底层操作类(SqlHelper)

这篇博客是我经过不断踩坑、重构后提炼出的 终极版 SQL 数据库帮助类 (SqlHelper)

1、参数化防注入:抵御黑客攻击的基本素养。

2、异常向上抛出 (throw):底层不处理 UI 弹窗,报错直接抛给调用者(界面层)去捕获并展示。

3、严格的资源释放:全套 using 嵌套,绝不浪费连接池。

4、命名规范:摒弃拼音和随意命名,采用标准的企业级命名法。

核心代码:SqlHelper.cs 完整源码

/// <summary>
/// SQL Server 数据库操作帮助类 (工业级封装)
/// </summary>
public class SqlHelper
{
    // 1. 获取全局配置的连接字符串
    // 专业写法通常需要从 App.config 或 appsettings.json 中读取
    private static readonly string connectionString = "";

    /// <summary>
    /// 多值查询 (返回 DataTable)
    /// 适用场景:SELECT 查询多行多列,用于绑定 DataGridView、ListView 等
    /// </summary>
    /// <param name="sqlText">包含 @ 占位符的完整 SQL 查询语句</param>
    /// <param name="parameters">与 SQL 语句中 @ 占位符对应的参数数组(可变参数,不传则无参查询)</param>
    /// <returns>装载了查询结果的 DataTable 对象。即使查无数据,也会返回一个空的 DataTable 而不是 null</returns>
    /// <exception cref="Exception">当底层数据库连接失败或 SQL 语法错误时,将底层的 SqlException 包装后向上抛出</exception>
    public static DataTable ExecuteQuery(string sqlText, params SqlParameter[] parameters)
    {
        using (SqlConnection conn = new SqlConnection(connectionString))
        {
            using (SqlCommand cmd = new SqlCommand(sqlText, conn))
            {
                // 工业级细节1:设置命令超时时间(默认通常是30秒,遇到复杂查询可适当调大)
                cmd.CommandTimeout = 30;

                if (parameters != null && parameters.Length > 0)
                {
                    cmd.Parameters.AddRange(parameters);
                }

                using (SqlDataAdapter adapter = new SqlDataAdapter(cmd))
                {
                    DataTable dt = new DataTable();
                    try
                    {
                        adapter.Fill(dt);
                        return dt;
                    }
                    catch (SqlException ex)
                    {
                        // 工业级细节2:捕获特定的数据库异常
                        // 在这里可以接入日志框架写入本地 txt 或系统日志
                        // Log.Error("数据库查询出错: " + sqlText, ex);

                        // 工业级细节3:将异常原封不动地抛给上层界面层!绝不隐瞒!
                        throw new Exception("底层执行查询报错:" + ex.Message, ex);
                    }
                }
            }
        }
    }

    /// <summary>
    /// 单值查询 (返回 Object)
    /// 适用场景:SELECT COUNT(*), SELECT MAX(ID) 等只返回第一行第一列的查询
    /// </summary>
    /// <param name="sqlText">包含 @ 占位符的单值查询 SQL 语句</param>
    /// <param name="parameters">与 SQL 语句中 @ 占位符对应的参数数组(可变参数)</param>
    /// <returns>返回结果集的第一行第一列数据(object 类型)。若数据库对应字段为空 (DBNull),则返回 null</returns>
    /// <exception cref="Exception">当底层数据库连接失败或 SQL 语法错误时抛出异常</exception>
    public static object ExecuteScalar(string sqlText, params SqlParameter[] parameters)
    {
        using (SqlConnection conn = new SqlConnection(connectionString))
        {
            using (SqlCommand cmd = new SqlCommand(sqlText, conn))
            {
                if (parameters != null && parameters.Length > 0)
                {
                    cmd.Parameters.AddRange(parameters);
                }

                try
                {
                    conn.Open();
                    object result = cmd.ExecuteScalar();

                    // 工业级细节4:处理 DBNull,数据库的空值在C#里是 DBNull.Value,不是 null
                    if (result == DBNull.Value)
                    {
                        return null;
                    }
                    return result;
                }
                catch (SqlException ex)
                {
                    throw new Exception("底层执行单值查询报错:" + ex.Message, ex);
                }
            }
        }
    }

    /// <summary>
    /// 数据修改 (返回受影响的行数 int)
    /// 适用场景:执行 INSERT(增), UPDATE(改), DELETE(删)操作
    /// </summary>
    /// <param name="sqlText">包含 @ 占位符的非查询类 SQL 语句</param>
    /// <param name="parameters">与 SQL 语句中 @ 占位符对应的参数数组(可变参数)</param>
    /// <returns>返回当前 SQL 操作成功影响的数据库行数</returns>
    /// <exception cref="Exception">遇到主键冲突、外键约束失败或 SQL 语法错误时抛出异常,需由界面层捕获并提示</exception>
    public static int ExecuteNonQuery(string sqlText, params SqlParameter[] parameters)
    {
        using (SqlConnection conn = new SqlConnection(connectionString))
        {
            using (SqlCommand cmd = new SqlCommand(sqlText, conn))
            {
                if (parameters != null && parameters.Length > 0)
                {
                    cmd.Parameters.AddRange(parameters);
                }

                try
                {
                    conn.Open();
                    return cmd.ExecuteNonQuery();
                }
                catch (SqlException ex)
                {
                    // 遇到主键冲突、外键约束等数据库报错,直接抛给界面层提示用户
                    throw new Exception("底层执行数据更新报错:" + ex.Message, ex);
                }
            }
        }
    }
}

💡 进阶小贴士:为什么要加 readonly?

细心的朋友可能会发现,我在声明连接字符串时加了一个 readonly(只读)关键字。这是干嘛用的?

它的核心作用是防篡改、防手抖。
在企业级开发中,代码动辄成千上万行。数据库连接字符串(connectionString)是整个数据交互的“命脉”。如果不加限制,万一哪天在写某个业务逻辑时不小心敲错代码,把它给重新赋值覆盖了,整个系统瞬间就会因为连不上数据库而瘫痪。

加上 readonly 后,就相当于给这个变量上了“死锁”:

只能在声明时或构造方法中赋值。

彻底锁死:程序运行期间,任何企图修改它的代码都会被编译器直接划红线拦截,连编译都过不去。

为什么不用 const(常量)?
因为 const 过于死板,必须在写代码时就把值写死。而 readonly 是“外柔内刚”,它允许我们在程序刚启动的那一瞬间,去动态读取外部的配置文件(比如 App.config),拿到值后再立刻锁死,完美契合工业级开发的配置读取需求!

更多推荐